Columns

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.

HK

Hoang Kowalski

hoang.kowalski1@example.com

Design
$127,691.00
Mar 6, 2024
BN

Bruno Nguyen

bruno.nguyen2@example.com

Growth
$105,146.00
Mar 6, 2024
BD

Bruno Dubois

bruno.dubois3@example.com

Design
$86,538.00
Feb 5, 2021
FH

Farid Haddad

farid.haddad4@example.com

Growth
$76,443.00
May 4, 2022
JY

Jonas Yilmaz

jonas.yilmaz5@example.com

Core
$129,812.00
Nov 13, 2024
QT

Quyen Tanaka

quyen.tanaka6@example.com

Growth
$72,011.00
Feb 17, 2019
6 rows
// 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

typeRenders
textPlain text
numberIntl.NumberFormat
currencyIntl currency style
percentIntl percent style
dateIntl.DateTimeFormat
datetimeDate plus time
booleanAn icon per state
badgesv5ui Badge
usersv5ui User
progresssv5ui Progress
ratingsv5ui Rating, readonly
linksv5ui Link
actionsA 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.

Name
number
currency USD
percent (ratio)
percent (whole)
Hoang Kowalski
127,691
$127,691.00
2%
87%
Bruno Nguyen
105,146
$105,146.00
45%
35%
Bruno Dubois
86,538
$86,538.00
35%
74%
Farid Haddad
76,443
$76,443.00
99%
17%
Jonas Yilmaz
129,812
$129,812.00
2%
12%
Quyen Tanaka
72,011
$72,011.00
72%
65%
6 rows
// 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.

Name
date
date long
datetime
Hoang Kowalski
Mar 6, 2024
March 6, 2024
Mar 6, 2024, 12:00 AM
Bruno Nguyen
Mar 6, 2024
March 6, 2024
Mar 6, 2024, 12:00 AM
Bruno Dubois
Feb 5, 2021
February 5, 2021
Feb 5, 2021, 12:00 AM
Farid Haddad
May 4, 2022
May 4, 2022
May 4, 2022, 12:00 AM
Jonas Yilmaz
Nov 13, 2024
November 13, 2024
Nov 13, 2024, 12:00 AM
Quyen Tanaka
Feb 17, 2019
February 17, 2019
Feb 17, 2019, 12:00 AM
6 rows
// 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.

user
badge
boolean
progress
rating
HK

Hoang Kowalski

hoang.kowalski1@example.com

invited
BN

Bruno Nguyen

bruno.nguyen2@example.com

invited
BD

Bruno Dubois

bruno.dubois3@example.com

active
FH

Farid Haddad

farid.haddad4@example.com

suspended
JY

Jonas Yilmaz

jonas.yilmaz5@example.com

active
QT

Quyen Tanaka

quyen.tanaka6@example.com

suspended
6 rows
// 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.

FieldType
labelstring
iconstring
onSelect(row: TRow) => void
disabledboolean
destructiveboolean

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.

Name
Country (no type)
Country (type: 'text')
Country (emptyText)
Salary
Hoang Kowalski
Poland
Poland
Poland
$127,691.00
Bruno Nguyen
—
—
Unknown
Not set
Bruno Dubois
—
—
Unknown
$86,538.00
3 rows
// 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.

Name
Team
Salary
Hoang Kowalski
Design
$127,691.00 senior
Bruno Nguyen
Growth
$105,146.00
Bruno Dubois
Design
$86,538.00
Farid Haddad
Growth
$76,443.00
Jonas Yilmaz
Core
$129,812.00 senior
Quyen Tanaka
Growth
$72,011.00
6 rows
<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.

OptionApplies to
localenumber, currency, percent, date, datetime
numberFormatnumber, currency, percent
currencycurrency
wholePercentpercent
dateFormatdate, datetime
colorsbadge
fallbackColorbadge
avataruser
descriptionuser
maxprogress, rating
hreflink
targetlink
actionsactions
trueIconboolean
falseIconboolean
emptyTextevery type