Skip to main content
Urbicon UI

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

Per-column snippets transform raw values into semantic badges and visual progress indicators.
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

Combine avatar initials, name, subtitle, and inline badges for information-dense rows.
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

A background colour derived from the value, so a pattern across rows is visible without reading them.
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

One snippet renders every column. resolveColumnId tells you which one you are in.
Name
Role
Department
Location
Emma WilsonStaff EngineerPlatform Berlin
Liam ChenProduct DesignerDesign Hamburg
Sofia MartinezEngineering ManagerPlatform Munich
James ParkFrontend DeveloperProduct 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.