Styling a headless table
With the headless engine you render your own markup, so you own every pixel of
styling - there is no grid stylesheet to fight. The same createSvGrid engine
can drive a minimal underlined table, a fully bordered grid, or floating row
cards. Flip the controls in the demo; the engine and rows are identical, only
the CSS changes.
The mindset
The engine hands you getHeaderGroups() and getRowModel().rows; how you turn
them into DOM - <table>, CSS grid, flexbox, divs - and how you style them is
entirely yours. Nothing below is prescribed; it's just what the demo does.
Track the site theme with --sg-* tokens
The grid's theming is a set of CSS custom properties (--sg-bg, --sg-fg,
--sg-border, --sg-header-bg, --sg-muted, --sg-accent, …). Use the same
tokens in your headless CSS and your table automatically follows the app's
light / dark theme and any per-instance overrides - no extra work.
.my-table { color: var(--sg-fg, #0f172a); font-size: 13px; }
.my-table th {
background: var(--sg-header-bg, #f1f5f9);
border-bottom: 2px solid var(--sg-border, #e2e8f0);
}
.my-table td { border-bottom: 1px solid var(--sg-border, #eef2f7); }
Always keep a literal fallback (var(--sg-fg, #0f172a)) so the table is styled
even outside a themed container. See Tailwind & theming for the
full token list.
The usual affordances
These are one CSS rule each - the engine doesn't need to know about them.
/* Zebra striping */
.my-table tbody tr:nth-child(even) td {
background: color-mix(in oklab, var(--sg-muted) 8%, transparent);
}
/* Hover */
.my-table tbody tr:hover td {
background: color-mix(in oklab, var(--sg-accent, #6366f1) 8%, transparent);
}
/* Sticky header inside a scroll container */
.my-table thead th { position: sticky; top: 0; z-index: 1; }
/* Right-align numbers */
.my-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* Density: swap a class */
.dense th, .dense td { padding: 5px 10px; }
Sort affordance
Sorting is engine state, but the indicator is your markup - render it off the
sorting state you already control:
<th onclick={() => toggleSort(h.column.id)}>
{h.column.columnDef.header}
{#if sorting[0]?.id === h.column.id}
<span class="ind">{sorting[0].desc ? '▼' : '▲'}</span>
{/if}
</th>
Prefer Tailwind? Use classes instead of CSS
Because it's your markup, utility classes work exactly as they would on any
<table> - no wrapper, no :global:
<table class="w-full text-sm">
<thead>
<tr class="border-b-2 border-slate-200">
{#each headers as h}
<th class="px-3 py-2 text-left font-semibold cursor-pointer hover:text-indigo-500">
{h.column.columnDef.header}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each rows as r}
<tr class="border-b border-slate-100 even:bg-slate-50 hover:bg-indigo-50">
<!-- cells -->
</tr>
{/each}
</tbody>
</table>
See also
- Build a table from scratch - the render loop
- Headless virtualization - style a virtualized list
- Tailwind & theming tokens - the
--sg-*reference