Klarhimmel UI

Table

Stable

A semantic data table in a keyboard-scrollable frame, with sortable headers.

npx shadcn add @klarhimmel/table

When 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

Upcoming bookings
GuestRoomNightsTotal (SEK)Status
Nora LindqvistGarden room34 350Confirmed
Sven BergSeaside suite11 900Pending
Ida HolmLoft57 250Confirmed
Total913 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 Berg12025-03-02
    Nora Lindqvist32025-03-14
    Ali Karimi22025-03-28
    Ida Holm52025-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
    SelectGuestRoom
    Nora LindqvistGarden
    Sven BergSeaside
    Ida HolmLoft

    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

PropTypeDefaultDescription
captionstring-Required. Names the table (and the scroll region).
hideCaptionbooleanfalseKeep 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 selectedbooleanfalseMarks the row (aria-selected) and tints it.
classNamestring-Extra classes, merged after the component's own.

Accessibility

KeyDoes
TabReaches the scroll frame and any sort buttons
Arrow keysScroll the frame when it is focused
Enter / SpaceSorts 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