Custom Cells
Per-column snippets and global cell overrides for rich, data-driven cell rendering.
A column's cell property takes a snippet, and the table itself
takes one that covers every column at once. Both receive the row item and the resolved cell value.
Whatever they render, search, sort, grouping and summaries keep working on the accessor's output,
so a badge or a progress bar never changes what a column sorts by.
Status Badges & Progress Bars
Name | Role | Status | Projects |
|---|---|---|---|
Emma Wilson | Staff Engineer | active | 12 |
Liam Chen | Product Designer | active | 8 |
Sofia Martinez | Engineering Manager | active | 15 |
James Park | Frontend Developer | on-leave | 6 |
Aisha Patel | Data Scientist | active | 10 |
Noah Kim | DevOps Engineer | active | 9 |
{#snippet statusCell(item, value)}
<Badge
intent={value === 'active' ? 'success' : value === 'on-leave' ? 'warning' : 'danger'}
size="xs"
>{value}</Badge>
{/snippet}
{#snippet projectsCell(item, value)}
<div class="flex items-center gap-2">
<div class="h-1.5 w-16 rounded-full bg-surface-subtle">
<div class="h-full rounded-full bg-primary" style="width: {(value / 20) * 100}%" />
</div>
<span class="text-xs">{value}</span>
</div>
{/snippet}
<Table items={data} columns={[
{ accessor: 'name', title: 'Name', sortable: true },
{ accessor: 'role', title: 'Role' },
{ accessor: 'status', title: 'Status', cell: statusCell },
{ accessor: 'projects', title: 'Projects', cell: projectsCell }
]} />Rich Multi-Info Cells
Employee | Department | Salary | Status |
|---|---|---|---|
EW Emma Wilson emma@acme.dev | Platform | 142.000,00 € | active |
LC Liam Chen liam@acme.dev | Design | 98.000,00 € | active |
SM Sofia Martinez sofia@acme.dev | Platform | 165.000,00 € | active |
JP James Park james@acme.dev | Product | 92.000,00 € | on-leave |
AP Aisha Patel aisha@acme.dev | Data | 128.000,00 € | active |
NK Noah Kim noah@acme.dev | Platform | 115.000,00 € | active |
{#snippet employeeCell(item)}
<div class="flex items-center gap-3">
<div class="bg-primary-subtle text-primary flex h-8 w-8 shrink-0
items-center justify-center rounded-full text-xs font-bold">
{item.name.split(' ').map(n => n[0]).join('')}
</div>
<div class="min-w-0">
<p class="text-sm font-medium truncate">{item.name}</p>
<p class="text-xs text-text-tertiary truncate">{item.email}</p>
</div>
</div>
{/snippet}
{#snippet salaryCell(item, value)}
<div class="text-right">
<span class="text-sm font-semibold tabular-nums">
{value?.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}
</span>
{#if value > 130000}
<span class="ml-1.5 inline-block h-1.5 w-1.5 rounded-full bg-success" />
{/if}
</div>
{/snippet}Heat Map Cells
Name | Role | Dept. | Projects |
|---|---|---|---|
Emma Wilson | Staff Engineer | Platform | 12 |
Liam Chen | Product Designer | Design | 8 |
Sofia Martinez | Engineering Manager | Platform | 15 |
James Park | Frontend Developer | Product | 6 |
Aisha Patel | Data Scientist | Data | 10 |
Noah Kim | DevOps Engineer | Platform | 9 |
{#snippet heatCell(item, value)}
{@const pct = Math.min(value / 20, 1)}
{@const hue = pct * 142}
<div
class="mx-auto flex h-8 w-12 items-center justify-center rounded-lg
text-xs font-bold tabular-nums"
style="background: oklch(0.92 0.06 {hue}); color: oklch(0.35 0.12 {hue})"
>
{value}
</div>
{/snippet}
<Table items={data} columns={[
...,
{ accessor: 'projects', title: 'Projects', cell: heatCell, align: 'center' }
]} />Global Cell Override
Name | Role | Department | Location |
|---|---|---|---|
| Emma Wilson | Staff Engineer | Platform | Berlin |
| Liam Chen | Product Designer | Design | Hamburg |
| Sofia Martinez | Engineering Manager | Platform | Munich |
| James Park | Frontend Developer | Product | Remote |
<Table {items} {columns}>
{#snippet cell(item, value, column)}
{#if resolveColumnId(column) === 'department'}
<Badge variant="outlined" intent="neutral" size="xs">{value}</Badge>
{:else}
<span class="text-sm">{value}</span>
{/if}
{/snippet}
</Table>A cell that grows past a few lines is easier to keep as a component: column.component renders a Svelte component per cell
and always passes the row as item, with componentProps supplying anything else. It types
cleanly where a snippet's argument annotations do not. When several of these are set on one
column, the first of table cell, column.cell, column.component and column.formatter wins, in that order.