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
- Sparkline-cells recipe - a non-trivial component cell
- Heatmap-cells recipe - computed-function cell with conditional formatting
- Barcode-cells recipe - real SVG rendered per row
- Conditional row coloring - styling without a custom renderer
<SvGrid>reference -renderSnippet/renderComponenthelpers
Related articles
- Custom Cell Renderers with Svelte Snippets - Use Svelte 5 snippets to render badges, progress bars, and action buttons inside SvGrid cells - without breaking sort, filter, or keyboard navigation.
- Progress and Percentage Bar Cells in SvGrid - Build in-cell progress bars in your Svelte 5 data grid - with color thresholds, accessible markup, and sorting that still works.
- A Fill Handle (Drag to Fill) in SvGrid - Build a working spreadsheet-style fill handle on top of SvGrid's cell selection and editing - pointer tracking, range highlighting, series fill, and undo/redo integration all covered.