| Subject | From | Date |
|---|---|---|
| Kumo v1.0.0 released | Visal In | 5 seconds ago |
| New Job Offer | Cloudflare | 10 minutes ago |
| Daily Email Digest | Cloudflare | 1 hour ago |
<script lang="ts">
import { LayerCard, Table } from 'kumo-svelte';
</script>
<LayerCard class="p-0">
<Table>
<Table.Header>
<Table.Row>
<Table.Head>Subject</Table.Head>
<Table.Head>From</Table.Head>
<Table.Head>Date</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each emailData.slice(0, 3) as row (row.id)}
<Table.Row>
<Table.Cell>{row.subject}</Table.Cell>
<Table.Cell>{row.from}</Table.Cell>
<Table.Cell>{row.date}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table>
</LayerCard> Installation
Barrel
import { Table, TableBody, TableCell, TableCheckCell, TableCheckHead, TableFooter, TableHead, TableHeader, TableResizeHandle, TableRow } from 'kumo-svelte'; Granular
import { Table, TableBody, TableCell, TableCheckCell, TableCheckHead, TableFooter, TableHead, TableHeader, TableResizeHandle, TableRow } from 'kumo-svelte/components/table';Usage
<script lang="ts"> import { LayerCard, Table } from 'kumo-svelte';</script><LayerCard class="p-0"> <Table> <Table.Header> <Table.Row> <Table.Head>Name</Table.Head> <Table.Head>Email</Table.Head> <Table.Head>Role</Table.Head> </Table.Row> </Table.Header> <Table.Body> <Table.Row> <Table.Cell>John Doe</Table.Cell> <Table.Cell>john@example.com</Table.Cell> <Table.Cell>Admin</Table.Cell> </Table.Row> </Table.Body> </Table></LayerCard>Examples
With Checkboxes
Add row selection with Table.CheckHead and Table.CheckCell. Both accept onCheckedChange, which matches the underlying Checkbox component's
signature.
The older onValueChange prop still works but is deprecated and will be
removed in a future major version. Migrate by renaming the prop:
<!-- Before (deprecated) --><Table.CheckCell onValueChange={(checked) => toggleRow(id)} /><!-- After --><Table.CheckCell onCheckedChange={(checked) => toggleRow(id)} /> | Subject | From | Date | |
|---|---|---|---|
| Kumo v1.0.0 released | Visal In | 5 seconds ago | |
| New Job Offer | Cloudflare | 10 minutes ago | |
| Daily Email Digest | Cloudflare | 1 hour ago |
<script lang="ts">
import { LayerCard, Table } from 'kumo-svelte';
const rows = emailData.slice(0, 3);
let selectedIds = $state<Set<string>>(new Set());
function toggleRow(id: string) {
const next = new Set(selectedIds);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
selectedIds = next;
}
function toggleAll() {
selectedIds = selectedIds.size === rows.length ? new Set() : new Set(rows.map((row) => row.id));
}
</script>
<LayerCard class="p-0">
<Table>
<Table.Header>
<Table.Row>
<Table.CheckHead
checked={selectedIds.size === rows.length}
indeterminate={selectedIds.size > 0 && selectedIds.size < rows.length}
onCheckedChange={toggleAll}
aria-label="Select all rows"
/>
<Table.Head>Subject</Table.Head>
<Table.Head>From</Table.Head>
<Table.Head>Date</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each rows as row (row.id)}
<Table.Row>
<Table.CheckCell
checked={selectedIds.has(row.id)}
onCheckedChange={() => toggleRow(row.id)}
aria-label={`Select ${row.subject}`}
/>
<Table.Cell>{row.subject}</Table.Cell>
<Table.Cell>{row.from}</Table.Cell>
<Table.Cell>{row.date}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table>
</LayerCard> Compact Header
Use variant="compact" on Table.Header for a more condensed header style.
| Subject | From | Date |
|---|---|---|
| Kumo v1.0.0 released | Visal In | 5 seconds ago |
| New Job Offer | Cloudflare | 10 minutes ago |
| Daily Email Digest | Cloudflare | 1 hour ago |
<script lang="ts">
import { LayerCard, Table } from 'kumo-svelte';
</script>
<LayerCard class="p-0">
<Table>
<Table.Header variant="compact">
<Table.Row>
<Table.Head>Subject</Table.Head>
<Table.Head>From</Table.Head>
<Table.Head>Date</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each emailData.slice(0, 3) as row (row.id)}
<Table.Row>
<Table.Cell>{row.subject}</Table.Cell>
<Table.Cell>{row.from}</Table.Cell>
<Table.Cell>{row.date}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table>
</LayerCard> Selected Row
Use variant="selected" on Table.Row to highlight selected rows.
| Subject | From | Date | |
|---|---|---|---|
| Kumo v1.0.0 released | Visal In | 5 seconds ago | |
| New Job Offer | Cloudflare | 10 minutes ago | |
| Daily Email Digest | Cloudflare | 1 hour ago |
<script lang="ts">
import { LayerCard, Table } from 'kumo-svelte';
const rows = emailData.slice(0, 3);
let selectedIds = $state<Set<string>>(new Set(['2']));
function toggleRow(id: string) {
const next = new Set(selectedIds);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
selectedIds = next;
}
function toggleAll() {
selectedIds = selectedIds.size === rows.length ? new Set() : new Set(rows.map((row) => row.id));
}
</script>
<LayerCard class="p-0">
<Table>
<Table.Header>
<Table.Row>
<Table.CheckHead
checked={selectedIds.size === rows.length}
indeterminate={selectedIds.size > 0 && selectedIds.size < rows.length}
onCheckedChange={toggleAll}
aria-label="Select all rows"
/>
<Table.Head>Subject</Table.Head>
<Table.Head>From</Table.Head>
<Table.Head>Date</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each rows as row (row.id)}
<Table.Row variant={selectedIds.has(row.id) ? 'selected' : 'default'}>
<Table.CheckCell
checked={selectedIds.has(row.id)}
onCheckedChange={() => toggleRow(row.id)}
aria-label={`Select ${row.subject}`}
/>
<Table.Cell>{row.subject}</Table.Cell>
<Table.Cell>{row.from}</Table.Cell>
<Table.Cell>{row.date}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table>
</LayerCard> Fixed Layout with Column Sizes
For precise control over column widths, set layout="fixed" and use colgroup with col elements.
| Subject | From | Date |
|---|---|---|
| Kumo v1.0.0 released | Visal In | 5 seconds ago |
| New Job Offer | Cloudflare | 10 minutes ago |
| Daily Email Digest | Cloudflare | 1 hour ago |
| GitLab - New Comment | Rob Knecht | 1 day ago |
| Out of Office | Johnnie Lappen | 3 days ago |
<script lang="ts">
import { LayerCard, Table } from 'kumo-svelte';
</script>
<LayerCard class="p-0">
<Table layout="fixed">
<colgroup>
<col />
<col class="w-[150px]" />
<col class="w-[150px]" />
</colgroup>
<Table.Header>
<Table.Row>
<Table.Head>Subject</Table.Head>
<Table.Head>From</Table.Head>
<Table.Head>Date</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each emailData as row (row.id)}
<Table.Row>
<Table.Cell>{row.subject}</Table.Cell>
<Table.Cell>{row.from}</Table.Cell>
<Table.Cell>{row.date}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table>
</LayerCard> Sticky Column
Pin a column to the left or right edge of the scroll container with sticky="left" or sticky="right" on Table.Head and Table.Cell. The
component automatically adds an opaque background and gradient fade. Wrap the
table in an overflow-x-auto container.
| Subject | From | Date | Tags | Actions |
|---|---|---|---|---|
| Kumo v1.0.0 released | Visal In | 5 seconds ago | — | |
| New Job Offer | Cloudflare | 10 minutes ago | — | |
| Daily Email Digest | Cloudflare | 1 hour ago | promotion | |
| GitLab - New Comment | Rob Knecht | 1 day ago | — | |
| Out of Office | Johnnie Lappen | 3 days ago | — |
<script lang="ts">
import { Badge, Button, DropdownMenu, LayerCard, Table } from 'kumo-svelte';
import { DotsThree, Eye, PencilSimple, Trash } from 'phosphor-svelte';
</script>
<LayerCard class="w-full max-w-md overflow-x-auto p-0">
<Table>
<Table.Header>
<Table.Row>
<Table.Head>Subject</Table.Head>
<Table.Head>From</Table.Head>
<Table.Head>Date</Table.Head>
<Table.Head>Tags</Table.Head>
<Table.Head sticky="right">
<span class="sr-only">Actions</span>
</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each emailData as row (row.id)}
<Table.Row>
<Table.Cell class="whitespace-nowrap">{row.subject}</Table.Cell>
<Table.Cell class="whitespace-nowrap">{row.from}</Table.Cell>
<Table.Cell class="whitespace-nowrap">{row.date}</Table.Cell>
<Table.Cell class="whitespace-nowrap">
{#if row.tags}
<div class="inline-flex gap-1">
{#each row.tags as tag}
<Badge>{tag}</Badge>
{/each}
</div>
{:else}
—
{/if}
</Table.Cell>
<Table.Cell sticky="right" class="text-right">
<DropdownMenu>
<DropdownMenu.Trigger>
<Button variant="ghost" size="sm" shape="square" aria-label="More options">
<DotsThree weight="bold" size={16} />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item icon={Eye}>View</DropdownMenu.Item>
<DropdownMenu.Item icon={PencilSimple}>Edit</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item icon={Trash} variant="danger">Delete</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table>
</LayerCard> Compact Header with Sticky Column
Combining variant="compact" on Table.Header with sticky columns.
| Subject | From | Date | Tags | Actions |
|---|---|---|---|---|
| Kumo v1.0.0 released | Visal In | 5 seconds ago | — | |
| New Job Offer | Cloudflare | 10 minutes ago | — | |
| Daily Email Digest | Cloudflare | 1 hour ago | promotion | |
| GitLab - New Comment | Rob Knecht | 1 day ago | — | |
| Out of Office | Johnnie Lappen | 3 days ago | — |
<script lang="ts">
import { Badge, Button, DropdownMenu, LayerCard, Table } from 'kumo-svelte';
import { DotsThree, Eye, PencilSimple, Trash } from 'phosphor-svelte';
</script>
<LayerCard class="w-full max-w-md overflow-x-auto p-0">
<Table>
<Table.Header variant="compact">
<Table.Row>
<Table.Head>Subject</Table.Head>
<Table.Head>From</Table.Head>
<Table.Head>Date</Table.Head>
<Table.Head>Tags</Table.Head>
<Table.Head sticky="right">
<span class="sr-only">Actions</span>
</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each emailData as row (row.id)}
<Table.Row>
<Table.Cell class="whitespace-nowrap">{row.subject}</Table.Cell>
<Table.Cell class="whitespace-nowrap">{row.from}</Table.Cell>
<Table.Cell class="whitespace-nowrap">{row.date}</Table.Cell>
<Table.Cell class="whitespace-nowrap">
{#if row.tags}
<div class="inline-flex gap-1">
{#each row.tags as tag}
<Badge>{tag}</Badge>
{/each}
</div>
{:else}
—
{/if}
</Table.Cell>
<Table.Cell sticky="right" class="text-right">
<DropdownMenu>
<DropdownMenu.Trigger>
<Button variant="ghost" size="sm" shape="square" aria-label="More options">
<DotsThree weight="bold" size={16} />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item icon={Eye}>View</DropdownMenu.Item>
<DropdownMenu.Item icon={PencilSimple}>Edit</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item icon={Trash} variant="danger">Delete</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table>
</LayerCard> Full Example
Complete table with checkboxes, badges, action buttons, and fixed column widths.
| Subject | From | Date | ||
|---|---|---|---|---|
Kumo v1.0.0 released | Visal In | 5 seconds ago | ||
New Job Offer | Cloudflare | 10 minutes ago | ||
Daily Email Digest promotion | Cloudflare | 1 hour ago | ||
GitLab - New Comment | Rob Knecht | 1 day ago | ||
Out of Office | Johnnie Lappen | 3 days ago |
<script lang="ts">
import { Badge, Button, DropdownMenu, LayerCard, Table } from 'kumo-svelte';
import { DotsThree, EnvelopeSimple, Eye, PencilSimple, Trash } from 'phosphor-svelte';
let selectedIds = $state<Set<string>>(new Set(['2']));
function toggleRow(id: string) {
const next = new Set(selectedIds);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
selectedIds = next;
}
function toggleAll() {
selectedIds = selectedIds.size === emailData.length ? new Set() : new Set(emailData.map((row) => row.id));
}
</script>
<LayerCard class="w-full overflow-x-auto p-0">
<Table layout="fixed">
<colgroup>
<col />
<col />
<col style:width="150px" />
<col style:width="120px" />
<col style:width="50px" />
</colgroup>
<Table.Header>
<Table.Row>
<Table.CheckHead
checked={selectedIds.size === emailData.length}
indeterminate={selectedIds.size > 0 && selectedIds.size < emailData.length}
onCheckedChange={toggleAll}
aria-label="Select all rows"
/>
<Table.Head>Subject</Table.Head>
<Table.Head>From</Table.Head>
<Table.Head>Date</Table.Head>
<Table.Head></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each emailData as row (row.id)}
<Table.Row variant={selectedIds.has(row.id) ? 'selected' : 'default'}>
<Table.CheckCell
checked={selectedIds.has(row.id)}
onCheckedChange={() => toggleRow(row.id)}
aria-label={`Select ${row.subject}`}
/>
<Table.Cell>
<div class="flex items-center gap-2">
<EnvelopeSimple size={16} />
<span class="truncate">{row.subject}</span>
{#if row.tags}
<div class="ml-2 inline-flex gap-1">
{#each row.tags as tag}
<Badge>{tag}</Badge>
{/each}
</div>
{/if}
</div>
</Table.Cell>
<Table.Cell>
<span class="truncate">{row.from}</span>
</Table.Cell>
<Table.Cell>
<span class="truncate">{row.date}</span>
</Table.Cell>
<Table.Cell class="text-right">
<DropdownMenu>
<DropdownMenu.Trigger>
<Button variant="ghost" size="sm" shape="square" aria-label="More options">
<DotsThree weight="bold" size={16} />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item icon={Eye}>View</DropdownMenu.Item>
<DropdownMenu.Item icon={PencilSimple}>Edit</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item icon={Trash} variant="danger">Delete</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table>
</LayerCard> API Reference
Table
Root table component. Renders a semantic <table> element.
| Prop | Type | Default | Description |
|---|---|---|---|
| layout | 'auto' | 'fixed' | "auto" | Table layout algorithm. |
| class | string | - | Additional classes merged onto the root element. |
| children | Snippet | - | Table section and row children. |
Table.Header
Table header section. Renders <thead>. Set sticky to pin the header row to the top of the scroll container.
Table.Body
Table body section. Renders <tbody>.
Table.Row
Table row. Supports variant="selected" for highlighting.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | 'default' | 'selected' | "default" | Row visual state. |
Table.Head
Header cell. Renders <th>. Accepts sticky="left" or sticky="right" to pin the column.
Table.Cell
Body cell. Renders <td>. Accepts sticky="left" or sticky="right" to pin the column.
Table.CheckHead
Header cell with checkbox for "select all" functionality.
| Prop | Type | Default | Description |
|---|---|---|---|
| checked | boolean | false | Checked state for the select-all checkbox. |
| indeterminate | boolean | false | Indeterminate state for the select-all checkbox. |
| onCheckedChange | (checked: boolean) => void | - | Called when the checkbox's checked state changes. |
| onValueChange | (checked: boolean) => void | - | Deprecated. Use onCheckedChange instead. |
| label | string | - | Accessible label for the checkbox. |
| disabled | boolean | false | Disables the component. |
Table.CheckCell
Body cell with checkbox for row selection.
| Prop | Type | Default | Description |
|---|---|---|---|
| checked | boolean | false | Checked state for the row checkbox. |
| indeterminate | boolean | false | Indeterminate state for the row checkbox. |
| onCheckedChange | (checked: boolean) => void | - | Called when the checkbox's checked state changes. |
| onValueChange | (checked: boolean) => void | - | Deprecated. Use onCheckedChange instead. |
| label | string | - | Accessible label for the checkbox. |
| disabled | boolean | false | Disables the component. |
Table.ResizeHandle
Draggable handle for column resizing. Use with TanStack Table or custom resize logic.
TanStack Table Integration
For advanced features like sorting, filtering, and resizable columns, integrate with TanStack Table. The Table component is designed to work with TanStack's headless API.
<script lang="ts"> import { Table } from 'kumo-svelte';</script><Table layout="fixed"> <colgroup> {#each table.getAllColumns() as column} <col style:width={`${column.getSize()}px`} /> {/each} </colgroup> <Table.Header> {#each table.getHeaderGroups() as headerGroup} <Table.Row> {#each headerGroup.headers as header} <Table.Head> {flexRender(header.column.columnDef.header, header.getContext())} <Table.ResizeHandle onmousedown={header.getResizeHandler()} ontouchstart={header.getResizeHandler()} /> </Table.Head> {/each} </Table.Row> {/each} </Table.Header> <Table.Body> {#each table.getRowModel().rows as row} <Table.Row> {#each row.getVisibleCells() as cell} <Table.Cell> {flexRender(cell.column.columnDef.cell, cell.getContext())} </Table.Cell> {/each} </Table.Row> {/each} </Table.Body></Table>Accessibility
Semantic HTML
Table uses semantic <table>, <thead>, <tbody>, <th>, and <td> elements for proper screen reader navigation.
Checkbox Labels
Always provide aria-label for Table.CheckHead and Table.CheckCell to
describe their purpose.