@kirikirikil/table-kit
v0.0.8
Published
Svelte 5 + Tailwind v4 data-table components: resizable columns with per-table persisted widths, sortable headers, a virtualized body, and an optional sticky actions column.
Readme
@kirikirikil/table-kit
Svelte 5 + Tailwind v4 data-table components: resizable columns with per-table persisted widths, sortable headers, a virtualized body, and an optional sticky actions column.
Install
npm install @kirikirikil/table-kitPeer dependencies: svelte ^5, @lucide/svelte ^0.561.
Tailwind setup
The components style themselves with Tailwind v4 utility classes against shadcn-style CSS variable tokens (--background, --muted, --border, --ring, --foreground, …). Your project needs those tokens mapped (Tailwind v4 @theme inline + --color-*), as in a standard shadcn-svelte setup.
Tailwind v4's automatic content detection skips node_modules, so if the package's utilities aren't generated, add an explicit source:
@import "tailwindcss";
@source "../../node_modules/@kirikirikil/table-kit/dist";(In a pnpm workspace you can point at the package src instead.)
Usage
<script lang="ts">
import {
Table, TableHeader, TableRow, TableHead, TableHeadSortable,
TableBody, TableCell, sortTable
} from '@kirikirikil/table-kit';
let sortKey = $state<string | null>(null);
let direction = $state<'asc' | 'desc'>('asc');
const rows = $derived(sortKey ? sortTable(data, sortKey, direction) : data);
function toggleSort(key: string) {
if (sortKey === key) direction = direction === 'asc' ? 'desc' : 'asc';
else { sortKey = key; direction = 'asc'; }
}
</script>
<Table tableId="products" columnKeys={['name', 'sku', 'price', '_actions']}>
<TableHeader>
<TableRow>
<TableHeadSortable sortKey="name" currentSort={sortKey} direction={direction} onSort={toggleSort}>Name</TableHeadSortable>
<TableHeadSortable sortKey="sku" currentSort={sortKey} direction={direction} onSort={toggleSort}>SKU</TableHeadSortable>
<TableHeadSortable sortKey="price" currentSort={sortKey} direction={direction} onSort={toggleSort}>Price</TableHeadSortable>
<TableHead aria-hidden="true" />
<TableHead columnKey="_actions">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each rows as r (r.sku)}
<TableRow>
<TableCell>{r.name}</TableCell>
<TableCell>{r.sku}</TableCell>
<TableCell>{r.price}</TableCell>
<TableCell aria-hidden="true" />
<TableCell>
<button class="text-primary" onclick={() => edit(r)}>Edit</button>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>Sticky actions column
With a _actions last columnKey, keep the actions <th>/<td> the row's last child by rendering one empty filler cell (<TableCell aria-hidden="true" />, and a matching empty <TableHead aria-hidden="true" /> in the header) immediately before it. This keeps position: sticky; right: 0 working while the actions column stays flush right. Tables without an actions column need no filler cell.
API
| Component | Props |
|---|---|
| Table | tableId?, columnKeys?, defaultWidths?, actionsWidth?, maxHeight?, containerRef? |
| TableHeadSortable | sortKey, currentSort, direction, onSort (renders a resize handle inside a resizable Table) |
| VirtualTableBody | items, rowHeight? (default 41), overscan? (default 5), scrollContainer, row snippet (item, index) |
| Table / TableHeader / TableRow / TableHead / TableCell / TableBody | also exported as Root / Header / Row / Head / Cell / Body |
| helpers | sortTable(data, key, dir), sortTableBy(data, getValue, dir), cn |
Resizable columns clamp to 60–500 px and persist per table under localStorage key table-columns-<tableId>; leftover container space is distributed proportionally across them so the table fills its width. The sticky actions column auto-sizes to its content (max scrollWidth of the actions cells, clamped 60–500 px, re-measured on content changes); pass actionsWidth to override it explicitly.
License
MIT
