Cell renderer patterns

Every supported shape for the cell: slot on a ColumnDef, with the trade-offs for each.

The four shapes

cell: accepts one of:

Shape When to use
string A literal placeholder ("TBD"). Rare.
(ctx) => string | number Computed text, no DOM control.
(ctx) => renderSnippet(...) Inline Svelte 5 snippet defined in the same file.
(ctx) => renderComponent(...) Mount a real Svelte component imported from another file.

ctx is { row: Row<TData>, column, table, getValue } - the same context TanStack-style tables emit. Everywhere below ctx.row.original is your TData row.

1. Literal string

The examples on this page run against these rows:

<script lang="ts">
  import { SvGrid, tableFeatures, rowSortingFeature, columnFilteringFeature } from '@svgrid/grid'

  const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
</script>
{ field: 'amount', header: 'Amount', cell: 'TBD' }

Used when bootstrapping. The cell renders the literal across every row.

2. Computed function

{
  field: 'amount', header: 'Amount', align: 'right',
  cell: (ctx) => {
    const n = ctx.row.original.amount
    if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`
    if (n >= 1_000)     return `$${Math.round(n / 1_000)}k`
    return `$${n}`
  },
}

Cheapest custom renderer. No DOM control - the grid wraps the return value in its own <td>. Use for formatted text, status acronyms, computed totals.

For built-in formats (currency, percent, date, units) prefer the declarative format: field - it runs through Intl.NumberFormat cached per locale.

3. Inline snippet

<script lang="ts">
  import { renderSnippet, type ColumnDef } from '@svgrid/grid'

  type Sale = { amount: number; up: boolean }
  const columns: ColumnDef<typeof features, Sale>[] = [
    {
      field: 'amount', header: 'Amount', align: 'right',
      cell: (ctx) => renderSnippet(Pill, { sale: ctx.row.original }),
    },
  ]
</script>

{#snippet Pill(props: { sale: Sale })}
  <span class="pill" class:up={props.sale.up} class:down={!props.sale.up}>
    {props.sale.up ? '▲' : '▼'} ${props.sale.amount.toLocaleString()}
  </span>
{/snippet}

<style>
  :global(.pill.up)   { background: rgba(34,197,94,0.18);  color: #15803d; }
  :global(.pill.down) { background: rgba(239,68,68,0.18);  color: #b91c1c; }
</style>

The snippet is local to the component, with access to its scope. Best for one-off renderers that need DOM control but don't merit their own file. The styles need :global because the snippet renders into the grid's body, not the consumer's component subtree.

4. Imported component

<script lang="ts">
  import { renderComponent } from '@svgrid/grid'
  import BadgeCell from './BadgeCell.svelte'

  const columns = [
    {
      field: 'amount', header: 'Amount',
      cell: (ctx) => renderComponent(BadgeCell, {
        label: `$${ctx.row.original.amount.toLocaleString()}`,
        tone:  ctx.row.original.up ? 'good' : 'bad',
      }),
    },
  ]
</script>
<!-- BadgeCell.svelte -->
<script lang="ts">
  let { label, tone }: { label: string; tone: 'good' | 'bad' } = $props()
</script>
<span class="bc" class:good={tone === 'good'} class:bad={tone === 'bad'}>{label}</span>

Use when the renderer is reused across columns or grids, or when it carries its own non-trivial state.

Headers, footers, editors - same shapes

The header: and footer: fields on ColumnDef accept the same four shapes - any function-valued header is rendered via the same path. So you can plug your own <HeaderToolbar /> component without forking <SvGrid>.

Custom inline editors use cellEditor: and follow the (ctx) => renderSnippet(...) pattern - see the 84-editor-types demo.

Try it

renderSnippet(snippet, params) is the whole custom-cell API. The snippet gets whatever you pass it, so a cell can read the row rather than just the value - which is what a status pill or a coloured figure needs.

<script lang="ts">
  import { SvGrid, renderSnippet, type GridColumns } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    age: number
    salary: number
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   age: 36, salary: 142000 },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', age: 45, salary: 168000 },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', age: 54, salary: 155000 },
    { id: 4, name: 'Radia Perlman',  department: 'Networking',  city: 'Seattle',  age: 49, salary: 161000 },
    { id: 5, name: 'Barbara Liskov', department: 'Platform',    city: 'Boston',   age: 52, salary: 172000 },
  ]

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 190 },
    { field: 'department', header: 'Department', width: 150 },
    { field: 'salary',     header: 'Salary',     width: 130, format: { type: 'currency', currency: 'USD' } },
  ]
</script>

{#snippet Money(props: { row: Person })}
  <span style="font-variant-numeric: tabular-nums; color: {props.row.salary > 160000 ? '#b45309' : 'inherit'}">
    {props.row.salary.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}
  </span>
{/snippet}

{#snippet Dept(props: { row: Person })}
  <span style="padding: 2px 8px; border-radius: 999px; background: color-mix(in srgb, currentColor 10%, transparent); font-size: 12px;">
    {props.row.department}
  </span>
{/snippet}

<SvGrid
  data={people}
  columns={[
    { field: 'name', header: 'Name', width: 190 },
    { field: 'department', header: 'Department', width: 160,
      cell: (ctx) => renderSnippet(Dept, { row: ctx.row.original }) },
    { field: 'salary', header: 'Salary', width: 140,
      cell: (ctx) => renderSnippet(Money, { row: ctx.row.original }) },
  ] satisfies GridColumns<Person>}
/>

See also

Related articles