Custom Cells
Per-column snippets and global cell overrides for rich, data-driven cell rendering.
A column's cell property takes a snippet that renders
that column's cells. <Table> also accepts a child snippet named cell that covers every column at once, and hands it the column
as a third argument to branch on. 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 |
<script lang="ts">
import { Table, type Column } from '@urbicon-ui/table';
import { Badge } from '@urbicon-ui/blocks';
const columns: Column<Employee>[] = [
{ accessor: 'name', title: 'Name', sortable: true },
{ accessor: 'role', title: 'Role' },
{ accessor: 'status', title: 'Status', cell: statusCell },
{ accessor: 'projects', title: 'Projects', cell: projectsCell }
];
</script>
{#snippet statusCell(item: Employee, value: unknown)}
<Badge
intent={value === 'active' ? 'success' : value === 'on-leave' ? 'warning' : 'danger'}
size="xs"
>{value}</Badge>
{/snippet}
{#snippet projectsCell(item: Employee, value: unknown)}
<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: {(Number(value) / 20) * 100}%"></div>
</div>
<span class="text-xs">{value}</span>
</div>
{/snippet}
<Table items={data} {columns} />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 |
const columns: Column<Employee>[] = [
{ accessor: 'name', title: 'Employee', cell: employeeCell },
{ accessor: 'department', title: 'Department', sortable: true },
{ accessor: 'salary', title: 'Salary', cell: salaryCell, dataType: 'number' },
{ accessor: 'status', title: 'Status', cell: statusCell }
];
{#snippet employeeCell(item: Employee)}
<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: Employee, value: unknown)}
<div class="text-right">
<span class="text-sm font-semibold tabular-nums">
{Number(value).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}
</span>
{#if Number(value) > 130000}
<span class="ml-1.5 inline-block h-1.5 w-1.5 rounded-full bg-success"></span>
{/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: Employee, value: unknown)}
{@const pct = Math.min(Number(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 |
<script lang="ts">
import { resolveColumnId, Table, type Column } from '@urbicon-ui/table';
import { Badge } from '@urbicon-ui/blocks';
</script>
<Table {items} {columns}>
{#snippet cell(item: Employee, value: unknown, column: Column<Employee>)}
{#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.
It receives the row as item, the table's size and the column's align, plus whatever componentProps(item) returns; the cell value is not
among them, so read it off item.
column.formatter is the last path, (value, item) => string | null, for a text
transform with no markup. When several are set on one column, the first of table cell, column.cell, column.component and column.formatter wins, in that order.