Cell Renderers
Set type on
a column and the matching sv5ui component renders the cell, with no snippet of your own.
Every Renderer
One grid, seven renderer families. Each cell is a real sv5ui component, so it inherits the theme, dark mode and accessibility of the rest of your app.
Hoang Kowalski
hoang.kowalski1@example.com
Bruno Nguyen
bruno.nguyen2@example.com
Bruno Dubois
bruno.dubois3@example.com
Farid Haddad
farid.haddad4@example.com
Jonas Yilmaz
jonas.yilmaz5@example.com
Quyen Tanaka
quyen.tanaka6@example.com
// Set type and the matching sv5ui component renders the cell, with no
// snippet of your own.
const columns: ColumnDef<Member>[] = [
{ id: 'name', type: 'user', typeOptions: { avatar: (m) => m.avatar,
description: (m) => m.email } },
{ id: 'team', type: 'badge', typeOptions: { colors: { Core: 'primary' } } },
{ id: 'salary', type: 'currency', typeOptions: { currency: 'USD' } },
{ id: 'progress', type: 'progress' },
{ id: 'rating', type: 'rating' },
{ id: 'active', type: 'boolean' },
{ id: 'joinedAt', type: 'date' }
];The Thirteen Types
| type | Renders |
|---|---|
text | Plain text |
number | Intl.NumberFormat |
currency | Intl currency style |
percent | Intl percent style |
date | Intl.DateTimeFormat |
datetime | Date plus time |
boolean | An icon per state |
badge | sv5ui Badge |
user | sv5ui User |
progress | sv5ui Progress |
rating | sv5ui Rating, readonly |
link | sv5ui Link |
actions | A DropdownMenu button |
Numbers
All three number families go through Intl, with formatters cached per configuration because a renderer runs on every visible cell. percent expects a 0 to 1 ratio unless you set wholePercent.
Either way the column's filter panel and its chips speak percentages, so a cell reading 5% is
found by typing 5. What the filter stores is what the row holds, 0.05 for a ratio column and 5 for a whole one, so a persisted filter and a server request keep the row's own units. The filtering page shows it both ways.
// number: Intl.NumberFormat, configured through numberFormat
{ id: 'salary', header: 'Raw', type: 'number' }
{ id: 'salary', header: 'Rounded', type: 'number',
typeOptions: { numberFormat: { maximumFractionDigits: 0 } } }
// currency: an ISO 4217 code, USD by default
{ id: 'salary', header: 'USD', type: 'currency' }
{ id: 'salary', header: 'EUR', type: 'currency',
typeOptions: { currency: 'EUR', locale: 'de-DE' } }
// percent expects a 0 to 1 ratio unless wholePercent says otherwise
{ id: 'completion', header: 'Ratio', type: 'percent' }
{ id: 'progress', header: '0 to 100', type: 'percent',
typeOptions: { wholePercent: true } }Dates
A value may be a Date, a timestamp or an ISO string. dateFormat is handed straight to Intl.DateTimeFormat.
A date-only string such as 2024-03-14 names a calendar day rather than an instant, so it is read as that day wherever the page is
open rather than as UTC midnight, which draws as the day before west of Greenwich. The same
reading is what the column sorts by, what its filter compares, what its editor opens on, and
what a CSV of it holds. A string that spells a day that does not exist is drawn as empty
rather than rolled forward into the next month.
// date and datetime take Intl.DateTimeFormat options
{ id: 'joinedAt', header: 'Default', type: 'date' }
{ id: 'joinedAt', header: 'Long', type: 'date',
typeOptions: { dateFormat: { dateStyle: 'long' } } }
{ id: 'joinedAt', header: 'With time', type: 'datetime',
typeOptions: { dateFormat: { dateStyle: 'medium', timeStyle: 'short' } } }
// Formatters are cached per configuration, because a renderer runs on
// every visible cell. Values may be a Date, a timestamp or an ISO string.Display Renderers
Badge, user, boolean, progress and rating cover most of what a dashboard row needs to say without a custom snippet.
Hoang Kowalski
hoang.kowalski1@example.com
Bruno Nguyen
bruno.nguyen2@example.com
Bruno Dubois
bruno.dubois3@example.com
Farid Haddad
farid.haddad4@example.com
Jonas Yilmaz
jonas.yilmaz5@example.com
Quyen Tanaka
quyen.tanaka6@example.com
// badge: map a value to a colour so statuses read at a glance
{
id: 'status',
type: 'badge',
typeOptions: {
colors: { active: 'success', invited: 'info', suspended: 'error' },
fallbackColor: 'surface'
}
}
// user: a name with an avatar and a secondary line
{
id: 'name',
type: 'user',
typeOptions: { avatar: (row) => row.avatar, description: (row) => row.email }
}
// boolean: two icons, overridable
{ id: 'active', type: 'boolean', align: 'center',
typeOptions: { trueIcon: 'lucide:circle-check', falseIcon: 'lucide:circle-x' } }
// progress and rating share max
{ id: 'progress', type: 'progress' } // max 100
{ id: 'rating', type: 'rating' } // max 5
{ id: 'score', type: 'rating', typeOptions: { max: 10 } }Links and Actions
A link column derives its href from the row, and an actions column opens a dropdown built from whatever the row allows.
Last action: none yet
// link: href defaults to the cell value
{ id: 'website', type: 'link' }
// or derive it from the row
{
id: 'email',
header: 'Email',
type: 'link',
typeOptions: { href: (row) => `mailto:${row.email}` }
}
{
id: 'name',
type: 'link',
typeOptions: { href: (row) => `/members/${row.id}`, target: '_blank' }
}
// Only navigating schemes are allowed through: http, https, mailto, tel,
// sms and ftp, plus relative urls. javascript: and data: render as text.Actions is a menu, not a row of buttons, so the column stays one tab stop no matter how many entries a row offers.
{
id: 'actions',
header: '',
width: 60,
align: 'center',
type: 'actions',
typeOptions: {
actions: (row) => [
{ label: 'View', icon: 'lucide:eye', onSelect: (r) => view(r) },
{ label: 'Edit', icon: 'lucide:pencil', onSelect: (r) => edit(r) },
{
label: 'Delete',
icon: 'lucide:trash-2',
destructive: true,
disabled: row.status === 'suspended',
onSelect: (r) => remove(r)
}
]
}
}
// Return an empty array and the column renders nothing for that row.RowAction
What one entry of an actions column looks like.
| Field | Type |
|---|---|
label | string |
icon | string |
onSelect | (row: TRow) => void |
disabled | boolean |
destructive | boolean |
Empty Values
Null, undefined and empty string all render as the default empty text, which is an em dash,
and typeOptions.emptyText replaces it per column. That holds whether or not the column declares a type:
the first two Country columns below are the same data, untyped and typed, and they read the
same.
A blank value never reaches the renderer, so a currency column does not print a zero it was
never given, and the empty text is what a cell snippet
replaces if it wants something else.
// Null, undefined and empty string all render as the default empty text,
// which is an em dash. A column with no type reads the same.
{ id: 'country', header: 'Country' }
{ id: 'country', header: 'Country', type: 'text' }
// typeOptions.emptyText replaces it per column
{ id: 'country', header: 'Country', typeOptions: { emptyText: 'Unknown' } }
{ id: 'salary', header: 'Salary', type: 'currency',
typeOptions: { currency: 'USD', emptyText: 'Not set' } }
// A blank value keeps the empty text rather than running the renderer, so
// a currency column does not print a zero it was never given.Custom Renderers
A cell snippet always wins over type, but
it does not have to throw the formatting away. Declare both and the snippet receives formatted,
the text the built-in renderer would have printed, so the column keeps its typeOptions and the snippet decorates around it.
It is undefined where the built-in rendering is a widget rather than text, since no string stands for a rating
or a progress bar, and it is only computed if the snippet reads it.
<script lang="ts">
// Declare type and cell together and the snippet is handed `formatted`:
// the text the built-in renderer would have printed for this value. The
// column keeps its own typeOptions and the snippet decorates around them,
// rather than rebuilding formatting the grid has already done.
//
// It is undefined where the built-in rendering is a widget - a rating, a
// progress bar, a boolean - since no string stands for one, and it is only
// computed if the snippet reads it.
</script>
<!-- A cell snippet always wins over type, so a column can graduate to a
custom renderer without changing anything else about it -->
{#snippet salaryCell({ row, formatted }: DataGridCellContext<Person>)}
<span class="flex items-center gap-2">
<span class="font-medium">{formatted}</span>
{#if row.salary > 120000}
<Badge label="senior" size="sm" color="warning" variant="soft" />
{/if}
</span>
{/snippet}typeOptions Reference
Only the fields belonging to the chosen type are read; the rest are ignored.
| Option | Applies to |
|---|---|
locale | number, currency, percent, date, datetime |
numberFormat | number, currency, percent |
currency | currency |
wholePercent | percent |
dateFormat | date, datetime |
colors | badge |
fallbackColor | badge |
avatar | user |
description | user |
max | progress, rating |
href | link |
target | link |
actions | actions |
trueIcon | boolean |
falseIcon | boolean |
emptyText | every type |