- Components
Table
A semantic data table in a keyboard-scrollable frame, with sortable headers.
npx shadcn add @klarhimmel/tableWhen to use
Use for
Rows that share columns, compared by eye: bookings, invoices, members.
Not for
Page layout, or one record's details.
Pairs with
Pagination, Badge, Checkbox, Menu.
Preview
| Guest | Room | Nights | Total (SEK) | Status |
|---|---|---|---|---|
| Nora Lindqvist | Garden room | 3 | 4 350 | Confirmed |
| Sven Berg | Seaside suite | 1 | 1 900 | Pending |
| Ida Holm | Loft | 5 | 7 250 | Confirmed |
| Total | 9 | 13 500 |
table.tsx
import { Theme } from '@/components/klarhimmel/theme/style';
import { Table, TableHead, TableBody, TableRow, TableHeader, TableRowHeader, TableCell } from '@/components/klarhimmel/ui/table';
export default function Example() {
return (
<Theme theme="brand">
<Table caption="Upcoming bookings">
<TableHead><tr><TableHeader>Guest</TableHeader><TableHeader align="right">Nights</TableHeader></tr></TableHead>
<TableBody>
<TableRow><TableRowHeader>Nora</TableRowHeader><TableCell align="right">3</TableCell></TableRow>
</TableBody>
</Table>
</Theme>
);
}Install
Import
import { Table, TableHead, TableBody, TableFoot, TableRow, TableHeader, TableRowHeader, TableCell } from '@/components/klarhimmel/ui/table';- Lands in
- components/klarhimmel/ui/table.tsx, plus cn() and the theme item if they are missing. Import theme.css after Tailwind.
- Render in
- Render it inside <Theme>, once near the root of your app. Tokens and fonts are scoped to it, so outside it the component is unstyled.
Examples
Sortable
Arrivals Sven Berg 1 2025-03-02 Nora Lindqvist 3 2025-03-14 Ali Karimi 2 2025-03-28 Ida Holm 5 2025-04-20 Sorted by arrival, ascending.
import { useState } from 'react'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TableRowHeader, type SortState } from '@/components/klarhimmel/ui/table'; const rows = [ { guest: 'Nora Lindqvist', nights: 3, arrival: '2025-03-14' }, { guest: 'Sven Berg', nights: 1, arrival: '2025-03-02' }, { guest: 'Ida Holm', nights: 5, arrival: '2025-04-20' }, { guest: 'Ali Karimi', nights: 2, arrival: '2025-03-28' }, ]; type Key = 'guest' | 'nights' | 'arrival'; export default function TableSortable() { const [sort, setSort] = useState<{ key: Key; dir: 'ascending' | 'descending' }>({ key: 'arrival', dir: 'ascending' }); const sorted = [...rows].sort((a, b) => (a[sort.key] < b[sort.key] ? -1 : a[sort.key] > b[sort.key] ? 1 : 0) * (sort.dir === 'ascending' ? 1 : -1)); const state = (k: Key): SortState => (sort.key === k ? sort.dir : 'none'); const next = (k: Key) => () => setSort((s) => ({ key: k, dir: s.key === k && s.dir === 'ascending' ? 'descending' : 'ascending' })); return ( <div className="flex w-[min(44rem,calc(100cqw-3rem))] flex-col gap-2"> <Table caption="Arrivals"> <TableHead> <tr> <TableHeader sort={state('guest')} onSort={next('guest')}>Guest</TableHeader> <TableHeader sort={state('nights')} onSort={next('nights')} align="right">Nights</TableHeader> <TableHeader sort={state('arrival')} onSort={next('arrival')}>Arrival</TableHeader> </tr> </TableHead> <TableBody> {sorted.map((r) => ( <TableRow key={r.guest}> <TableRowHeader>{r.guest}</TableRowHeader> <TableCell align="right">{r.nights}</TableCell> <TableCell>{r.arrival}</TableCell> </TableRow> ))} </TableBody> </Table> <p role="status" className="text-kh-small text-kh-muted">Sorted by {sort.key}, {sort.dir}.</p> </div> ); }Selectable
Pick guests to message Select Guest Room Nora Lindqvist Garden Sven Berg Seaside Ida Holm Loft 1 selected
import { useState } from 'react'; import { Checkbox } from '@/components/klarhimmel/ui/checkbox'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TableRowHeader } from '@/components/klarhimmel/ui/table'; const rows = ['Nora Lindqvist', 'Sven Berg', 'Ida Holm']; export default function TableSelectable() { const [picked, setPicked] = useState<string[]>(['Sven Berg']); const toggle = (n: string) => setPicked((p) => (p.includes(n) ? p.filter((x) => x !== n) : [...p, n])); return ( <div className="flex w-[min(44rem,calc(100cqw-3rem))] flex-col gap-2"> <Table caption="Pick guests to message"> <TableHead><tr><TableHeader><span className="sr-only">Select</span></TableHeader><TableHeader>Guest</TableHeader><TableHeader>Room</TableHeader></tr></TableHead> <TableBody> {rows.map((n, i) => ( <TableRow key={n} selected={picked.includes(n)}> <TableCell><Checkbox aria-label={`Select ${n}`} checked={picked.includes(n)} onCheckedChange={() => toggle(n)} /></TableCell> <TableRowHeader>{n}</TableRowHeader> <TableCell>{['Garden', 'Seaside', 'Loft'][i]}</TableCell> </TableRow> ))} </TableBody> </Table> <p role="status" className="text-kh-small text-kh-muted">{picked.length} selected</p> </div> ); }
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
caption | string | - | Required. Names the table (and the scroll region). |
hideCaption | boolean | false | Keep the caption for screen readers only. |
TableHeader sort / onSort | 'ascending' | 'descending' | 'none' / () => void | - | Makes the header a sort button and sets aria-sort. You sort the rows. |
TableHeader / TableCell align | 'left' | 'right' | 'left' | Right-align numbers. |
TableRow selected | boolean | false | Marks the row (aria-selected) and tints it. |
className | string | - | Extra classes, merged after the component's own. |
Accessibility
| Key | Does |
|---|---|
Tab | Reaches the scroll frame and any sort buttons |
Arrow keys | Scroll the frame when it is focused |
Enter / Space | Sorts by the focused header |
The caption names the table. Wide tables scroll inside a focusable region. You sort the rows; the header only reports and asks.
Accessibility