UI componentsdocs/ui/components/data.md

Data display

Present records: tables, lists, trees, boards, and disclosure of detail.

← Component reference

Table #

Static and sortable data tables.

Table #

Styled <table>; pass <thead> / <tbody> as children.

ProjectStatus
marketing-siteReady
docsBuilding
<Table striped fullwidth>
    <thead>
        <tr>
            <th scope="col">Project</th>
            <th scope="col">Status</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>marketing-site</td>
            <td>Ready</td>
        </tr>
        <tr>
            <td>docs</td>
            <td>Building</td>
        </tr>
    </tbody>
</Table>
PropTypeRequiredDefaultDescription
borderedbooleanBorders around every cell.
childrencontentTable parts: Thead, Tbody, Tfoot (or native elements).
fullwidthbooleanStretch to the container's width.
hoverablebooleanHighlight the row under the pointer.
narrowbooleanCompact cell padding.
stripedbooleanAlternate row shading.

Parts

Thead #

Table head section.

InvoiceAmount
INV-014$49.00
<Table>
    <Thead>
        <Tr>
            <Th scope="col">Invoice</Th>
            <Th scope="col">Amount</Th>
        </Tr>
    </Thead>
    <Tbody>
        <Tr>
            <Td>INV-014</Td>
            <Td>$49.00</Td>
        </Tr>
    </Tbody>
</Table>
PropTypeRequiredDefaultDescription
childrencontentRows or cell content.

Tbody #

Table body section.

INV-014$49.00
INV-013$49.00
<Table>
    <Tbody>
        <Tr>
            <Td>INV-014</Td>
            <Td>$49.00</Td>
        </Tr>
        <Tr>
            <Td>INV-013</Td>
            <Td>$49.00</Td>
        </Tr>
    </Tbody>
</Table>
PropTypeRequiredDefaultDescription
childrencontentRows or cell content.

Tfoot #

Table footer section.

INV-014$49.00
Total$49.00
<Table>
    <Tbody>
        <Tr>
            <Td>INV-014</Td>
            <Td>$49.00</Td>
        </Tr>
    </Tbody>
    <Tfoot>
        <Tr>
            <Th scope="row">Total</Th>
            <Td>$49.00</Td>
        </Tr>
    </Tfoot>
</Table>
PropTypeRequiredDefaultDescription
childrencontentRows or cell content.

Tr #

Table row (clickable when onClick is set).

INV-014Paid
<Table>
    <Tbody>
        <Tr>
            <Td>INV-014</Td>
            <Td>Paid</Td>
        </Tr>
    </Tbody>
</Table>
PropTypeRequiredDefaultDescription
childrencontentRows or cell content.
onClick(e: MouseEvent) => voidMakes the row clickable.

Th #

Table header cell.

Status
<Table>
    <Thead>
        <Tr>
            <Th scope="col">
                Status
            </Th>
        </Tr>
    </Thead>
</Table>
PropTypeRequiredDefaultDescription
abbrstringShort form of the header, read by screen readers.
childrencontentRows or cell content.
colspanstring | numberColumns the cell spans.
headersstringIds of the header cells describing this cell.
rowspanstring | numberRows the cell spans.
scope"row" | "col" | "rowgroup" | "colgroup"Which cells a header cell describes.

Td #

Table data cell.

Paid
<Table>
    <Tbody>
        <Tr>
            <Td>Paid</Td>
        </Tr>
    </Tbody>
</Table>
PropTypeRequiredDefaultDescription
abbrstringShort form of the header, read by screen readers.
childrencontentRows or cell content.
colspanstring | numberColumns the cell spans.
headersstringIds of the header cells describing this cell.
rowspanstring | numberRows the cell spans.
scope"row" | "col" | "rowgroup" | "colgroup"Which cells a header cell describes.

DataTable #

Sortable data grid with ARIA table semantics (rowgroups, aria-sort, header sort buttons). Sorting is stable; clicking cycles asc → desc → none.

Deploy
Branch
#128
main
102s
#127
feat/ui-kit
88s
#126
main
131s
<DataTable
    label="Deploys"
    rows={[
        { id: "128", branch: "main", duration: 102 },
        { id: "127", branch: "feat/ui-kit", duration: 88 },
        { id: "126", branch: "main", duration: 131 },
    ]}
    columns={[
        { id: "id", header: "Deploy", cell: (r: { id: string }) => `#${r.id}` },
        { id: "branch", header: "Branch", cell: (r: { branch: string }) => r.branch },
        {
            id: "duration",
            header: "Duration",
            cell: (r: { duration: number }) => `${r.duration}s`,
            sortValue: (r: { duration: number }) => r.duration,
        },
    ]}
/>

Slots: root body cell empty head header row sort

PropTypeRequiredDefaultDescription
columnsDataTableColumn<T>[]yesColumn definitions, in order.
rowsT[]yesRow data; each row needs a unique id.
defaultSortDataTableSortInitial sort (read once).
emptycontentShown when there are no rows.
labelstringAccessible name for the table.
onSortChange(sort: DataTableSort | null) => voidNotified after a header click changes the sort.

DataTableColumn

FieldTypeRequiredDescription
idstringyesColumn id (used by sorting).
headerstringyesColumn header text.
cell(row: T) => unknownyesRenders a row's cell for this column.
sortValue(row: T) => string | numberValue to sort by; the column is sortable when set.

DataTableSort

FieldTypeRequiredDescription
idstringyesId of the sorted column.
dir"asc" | "desc"yesSort direction.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

Description list #

Label / value pairs.

DescriptionList #

Label / value pairs (<dl>).

Region
eu-central-1
Runtime
Bun 1.3
Created
Sep 3, 2026
<DescriptionList
    items={[
        { label: "Region", value: "eu-central-1" },
        { label: "Runtime", value: "Bun 1.3" },
        { label: "Created", value: "Sep 3, 2026" },
    ]}
/>

Slots: root label row value

PropTypeRequiredDefaultDescription
itemsDescriptionItem[]yesLabel / value pairs, in order.

DescriptionItem

FieldTypeRequiredDescription
labelstringyesTerm (rendered as <dt>).
valuecontentyesDescription text or content (rendered as <dd>).

PropertyList #

Label / value pairs.

Status
Active
Owner
Ada Lovelace
Region
eu-central-1
<PropertyList
    items={[
        { label: "Status", value: "Active" },
        { label: "Owner", value: "Ada Lovelace" },
        { label: "Region", value: "eu-central-1" },
    ]}
/>

Slots: root label row value

PropTypeRequiredDefaultDescription
itemsPropertyItem[]yesProperties, in order.

PropertyItem

FieldTypeRequiredDescription
labelstringyesProperty name.
valuecontentyesProperty value (text or content).

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

List #

Plain and grouped lists.

List #

Styled list (ordered for numbers).

  • Install the package
  • Import the stylesheet
  • Render your first component
<List>
    <ListItem>Install the package</ListItem>
    <ListItem>Import the stylesheet</ListItem>
    <ListItem>Render your first component</ListItem>
</List>
PropTypeRequiredDefaultDescription
childrencontentListItems.
orderedbooleanRender <ol> instead of <ul>; fixed at mount.

Parts

ListItem #

List item with an optional icon.

  1. Install the package
  2. Import the stylesheet
<List ordered>
    <ListItem>Install the package</ListItem>
    <ListItem>Import the stylesheet</ListItem>
</List>

Slots: root body icon

PropTypeRequiredDefaultDescription
childrencontentItem content.
iconIconNameIcon instead of the bullet.

IconName — Material Design Icons path names used by <Icon />.

type IconName = | "check" | "x" | "plus" | "minus" | "search" | "user" | "users" | "settings" | "menu" | "home" | "heart" | "star" | "bell" | "mail" | "calendar" | "clock" | "edit" | "trash" | "copy" | "download" | "upload" | "link" | "external" | "info" | "warning" | "error" | "success" | "chevron-down" | "chevron-up" | "chevron-left" | "chevron-right" | "arrow-left" | "arrow-right" | "eye" | "eye-off" | "eye-outline" | "lock" | "unlock" | "filter" | "more" | "close" | "spinner" | "sun" | "moon" | "play" | "pause" | "refresh" | "share" | "image" | "file" | "folder" | "zap" | "phone" | "git" | "code";

ListGroup #

Bordered group of list rows.

  • Profile
  • Security
  • Notifications
<ListGroup>
    <ListGroupItem>Profile</ListGroupItem>
    <ListGroupItem>Security</ListGroupItem>
    <ListGroupItem>Notifications</ListGroupItem>
</ListGroup>
PropTypeRequiredDefaultDescription
childrencontentListGroupItems.
flushbooleanNo outer border or radius (for edge-to-edge use in cards).

Parts

ListGroupItem #

List-group row.

  • Profile
  • Security
<ListGroup>
    <ListGroupItem>Profile</ListGroupItem>
    <ListGroupItem>Security</ListGroupItem>
</ListGroup>

Slots: root item

PropTypeRequiredDefaultDescription
activebooleanMarks the current row.
childrencontentRow content.
disabledbooleanDims the row and blocks clicks.
onClick(e: MouseEvent) => voidMakes the row a button (fixed at mount); the <li> becomes the item slot.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

Timeline #

Vertical timeline.

  1. Build started

    09:41
  2. Tests passed

    09:43
  3. Deployed to production

    09:44
<Timeline>
    <TimelineItem title="Build started" bullet="play">
        09:41
    </TimelineItem>
    <TimelineItem title="Tests passed" bullet="check">
        09:43
    </TimelineItem>
    <TimelineItem title="Deployed to production" bullet="zap" active>
        09:44
    </TimelineItem>
</Timeline>
PropTypeRequiredDefaultDescription
childrencontentTimelineItems, oldest or newest first.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

Parts

TimelineItem #

Timeline entry with a bullet, title and content.

  1. Deployed to production

    09:44 by Ada
<Timeline>
    <TimelineItem title="Deployed to production" bullet="zap" active>
        09:44 by Ada
    </TimelineItem>
</Timeline>

Slots: root body bullet content title

PropTypeRequiredDefaultDescription
activebooleanHighlights this event (e.g. the latest).
bulletIconNameIcon in the bullet.
childrencontentEvent detail under the title.
titlestringEvent title.

IconName — Material Design Icons path names used by <Icon />.

type IconName = | "check" | "x" | "plus" | "minus" | "search" | "user" | "users" | "settings" | "menu" | "home" | "heart" | "star" | "bell" | "mail" | "calendar" | "clock" | "edit" | "trash" | "copy" | "download" | "upload" | "link" | "external" | "info" | "warning" | "error" | "success" | "chevron-down" | "chevron-up" | "chevron-left" | "chevron-right" | "arrow-left" | "arrow-right" | "eye" | "eye-off" | "eye-outline" | "lock" | "unlock" | "filter" | "more" | "close" | "spinner" | "sun" | "moon" | "play" | "pause" | "refresh" | "share" | "image" | "file" | "folder" | "zap" | "phone" | "git" | "code";

Tree #

WAI-ARIA tree: one tab stop, ↑ ↓ move, → expands / enters, ← collapses / goes to parent, Home End, Enter/Space select, type-ahead. Node icons and badges, filter for search, icons="auto" for folder/file icons.

  • apps2
    • adminlocked
  • packages
    • ui
      • button.tsx
      • tree.tsx
      • styles.css
  • README.md

Open: button.tsx

function Example() {
    const selected = signal("button.tsx");
    const query = signal("");
    const files: TreeNode[] = [
        {
            id: "apps",
            label: "apps",
            badge: 2,
            children: [
                { id: "web", label: "web", children: [{ id: "client.tsx", label: "client.tsx" }] },
                { id: "admin", label: "admin", disabled: true, badge: "locked" },
            ],
        },
        {
            id: "packages",
            label: "packages",
            children: [
                {
                    id: "ui",
                    label: "ui",
                    children: [
                        { id: "button.tsx", label: "button.tsx" },
                        { id: "tree.tsx", label: "tree.tsx" },
                        { id: "styles.css", label: "styles.css", icon: "code" },
                    ],
                },
            ],
        },
        { id: "readme", label: "README.md", icon: "info" },
    ];
    return (
        <Stack gap="0.5rem" style={{ "max-width": "22rem" }}>
            <SearchInput
                aria-label="Filter files"
                placeholder="Filter files"
                value={query()}
                onChange={query.set}
            />
            <Tree
                label="Workspace"
                icons="auto"
                filter={query()}
                defaultExpanded={["apps", "packages", "ui"]}
                value={selected()}
                onChange={selected.set}
                data={files}
            />
            <Text muted>Open: {selected()}</Text>
        </Stack>
    );
}

Slots: root badge empty group icon item label row toggle

PropTypeRequiredDefaultDescription
dataTreeNode[]yesRoot nodes.
defaultExpandedstring[]Node ids expanded on mount.
emptyLabelcontent"No matches"Shown when filter matches nothing (default "No matches").
expandOnClickbooleanClicking a parent row also expands / collapses it (default true).
filterstring""Show only nodes whose label contains this text (case-insensitive), plus their ancestors, expanded, with the match highlighted.
icons"auto" | "none""auto": folder / file icons for nodes without their own icon.
labelstringAccessible name for the tree.
onChange(id: string) => voidCalled with the id of the node the user selects.
onToggle(id: string, open: boolean) => voidCalled when a node is expanded or collapsed by the user.
valuestringId of the selected node.

TreeNode

FieldTypeRequiredDescription
idstringyesNode id, passed to onChange and matched against value.
labelstringyesNode text (also used by type-ahead and filter).
childrenTreeNode[]Child nodes; a node with children can expand.
iconcontentLeading icon: an icon name or any content.
badgecontentTrailing content, e.g. a count or status badge.
disabledbooleanShown and focusable, but can't be selected.

IconName — Material Design Icons path names used by <Icon />.

type IconName = | "check" | "x" | "plus" | "minus" | "search" | "user" | "users" | "settings" | "menu" | "home" | "heart" | "star" | "bell" | "mail" | "calendar" | "clock" | "edit" | "trash" | "copy" | "download" | "upload" | "link" | "external" | "info" | "warning" | "error" | "success" | "chevron-down" | "chevron-up" | "chevron-left" | "chevron-right" | "arrow-left" | "arrow-right" | "eye" | "eye-off" | "eye-outline" | "lock" | "unlock" | "filter" | "more" | "close" | "spinner" | "sun" | "moon" | "play" | "pause" | "refresh" | "share" | "image" | "file" | "folder" | "zap" | "phone" | "git" | "code";

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

SortableList #

Reorderable list with keyboard-accessible move buttons.

  • Install
  • Test
  • Deploy
function Example() {
    const steps = signal([
        { id: "install", label: "Install" },
        { id: "test", label: "Test" },
        { id: "deploy", label: "Deploy" },
    ]);
    return <SortableList items={steps()} onChange={steps.set} />;
}

Slots: root actions item label

PropTypeRequiredDefaultDescription
itemsSortableItem[]yesItems in their current order (controlled).
onChange(items: SortableItem[]) => voidyesCalled with the reordered items (drag or the move buttons).

SortableItem

FieldTypeRequiredDescription
idstringyesItem id (stable across reorders).
labelstringyesItem text.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

KanbanBoard #

Horizontal board of kanban columns. With onMove, cards (cardId) can be dragged between columns (columnId) or moved with Alt+←/→ (column) and Alt+↑/↓ (position); moves are announced to screen readers.

Todo2
In progress1
Done1
Interact with the example: callbacks show up here.
function Example() {
    type Card = { id: string; title: string; meta: string; column: string };
    const columns = [
        { id: "todo", title: "Todo" },
        { id: "doing", title: "In progress" },
        { id: "done", title: "Done" },
    ];
    const cards = signal<Card[]>([
        { id: "c-1", title: "Audit form labels", meta: "#418", column: "todo" },
        { id: "c-2", title: "Toast pause on hover", meta: "#421", column: "todo" },
        { id: "c-3", title: "Kanban keyboard moves", meta: "#402", column: "doing" },
        { id: "c-4", title: "Dark theme tokens", meta: "#389", column: "done" },
    ]);
    const move = (cardId: string, toColumn: string, index: number) => {
        const card = cards().find((c) => c.id === cardId);
        if (!card) return;
        const rest = cards().filter((c) => c.id !== cardId);
        const target = rest.filter((c) => c.column === toColumn);
        const before = target[index];
        const at = before ? rest.indexOf(before) : rest.length;
        cards.set([...rest.slice(0, at), { ...card, column: toColumn }, ...rest.slice(at)]);
    };
    return (
        <KanbanBoard label="Sprint 14" onMove={move}>
            <For each={columns}>
                {(column) => (
                    <KanbanColumn
                        title={column.title}
                        columnId={column.id}
                        count={cards().filter((c) => c.column === column.id).length}
                    >
                        <For each={cards().filter((c) => c.column === column.id)}>
                            {(card) => (
                                <KanbanCard
                                    cardId={card.id}
                                    title={card.title}
                                    meta={card.meta}
                                    onClick={() => {}}
                                />
                            )}
                        </For>
                    </KanbanColumn>
                )}
            </For>
        </KanbanBoard>
    );
}
PropTypeRequiredDefaultDescription
childrencontentThe board's KanbanColumns.
labelstring"Board"Accessible name for the board. Default "Board".
onMove(cardId: string, toColumnId: string, index: number) => voidCalled when a card is dropped on a column or moved with Alt+arrow keys. index is the position in the target column (without the moved card). Update your data; the board keeps focus on the moved card.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

Parts

KanbanColumn #

Kanban column with title, count and cards.

In review1
<KanbanBoard label="Review board">
    <KanbanColumn title="In review" columnId="review" count={1}>
        <KanbanCard cardId="c-7" title="Dark theme for charts" meta="#412 · Ada" />
    </KanbanColumn>
</KanbanBoard>

Slots: root body count header title

PropTypeRequiredDefaultDescription
titlestringyesColumn heading (also its accessible name).
childrencontentThe column's KanbanCards.
columnIdstringColumn id reported to KanbanBoard onMove; enables dropping cards here.
countnumberNumber shown next to the title, e.g. the card count.

KanbanCard #

Card in a kanban column.

Backlog
<KanbanBoard label="Backlog board">
    <KanbanColumn title="Backlog" columnId="backlog">
        <KanbanCard cardId="c-9" title="Audit form labels" meta="#418 · Grace" />
    </KanbanColumn>
</KanbanBoard>

Slots: root meta title

PropTypeRequiredDefaultDescription
titlestringyesCard title.
cardIdstringCard id reported to KanbanBoard onMove; makes the card draggable.
metastringSmall line, e.g. issue number or assignee.
onClick(e: MouseEvent) => voidCalled when the card is clicked (e.g. to open it).

Accordion & disclosure #

Show and hide detail.

Accordion #

Disclosure list. Panels stay mounted (state and focus survive toggling) and animate height via grid-template-rows.

You're billed monthly per seat.

Yes — your plan ends at the period's close.

In the EU (Frankfurt) by default.
function Example() {
    const openItem = signal<string | null>("billing");
    return (
        <Accordion
            value={openItem() ?? ""}
            onChange={(id: string | null) => openItem.set(id)}
            items={[
                {
                    id: "billing",
                    title: "How does billing work?",
                    content: "You're billed monthly per seat.",
                },
                {
                    id: "cancel",
                    title: "Can I cancel anytime?",
                    content: "Yes — your plan ends at the period's close.",
                },
                {
                    id: "data",
                    title: "Where is my data stored?",
                    content: "In the EU (Frankfurt) by default.",
                },
            ]}
        />
    );
}

Slots: root chevron content icon item panel subtitle title trigger

PropTypeRequiredDefaultDescription
itemsAccordionItem[]yesThe sections, in order.
onChange((id: string | null) => void) | ((ids: string[]) => void)yesCalled with the id to open, or null when the open item is closed.
valuestring | string[]yesId of the open item, or null when all are closed.
chevroncontentCustom chevron (any node); null hides it.
multiplebooleanSingle mode: one item open at a time (value is an id or null).
variant"default" | "separated" | "flush""default"separated renders each item as its own card.

AccordionItem

FieldTypeRequiredDescription
idstringyesItem id, passed to onChange and matched against value.
titlecontentyesHeader text or content (the toggle button's label).
contentcontentyesPanel content shown when the item is open.
disabledbooleanThe header can't be toggled.
iconcontentLeading icon / avatar next to the title.
subtitlecontentSecondary line under the title.

Collapse #

Show/hide region.

Collapsible content animates its height when toggled.

function Example() {
    const open = signal(true);
    return (
        <Stack gap="0.5rem">
            <Button size="sm" variant="outline" aria-expanded={open()} onClick={() => open.set(!open())}>
                {open() ? "Hide details" : "Show details"}
            </Button>
            <Collapse open={open()}>
                <Text>Collapsible content animates its height when toggled.</Text>
            </Collapse>
        </Stack>
    );
}
PropTypeRequiredDefaultDescription
openbooleanyesWhether the content is shown; height animates on change.
childrencontentContent to show or hide.

Spoiler #

Height-clamped content with a toggle.

Arachne renders on the server and hydrates on the client without re-running component bodies. Signals track exactly which DOM nodes depend on which values, so updates touch only what changed. Components share one customization system for classes, styles and slots.
<Spoiler maxHeight={48} showLabel="Show more" hideLabel="Show less">
    Arachne renders on the server and hydrates on the client without re-running component
    bodies. Signals track exactly which DOM nodes depend on which values, so updates touch only
    what changed. Components share one customization system for classes, styles and slots.
</Spoiler>

Slots: root content toggle

PropTypeRequiredDefaultDescription
childrencontentLong content to clip.
hideLabelstring"Show less"Button text while expanded.
maxHeightnumber80Collapsed height in pixels.
showLabelstring"Show more"Button text while collapsed.

Details #

Native <details> disclosure (listen with onToggle).

Why is my deploy queued?
Free plans run one build at a time; later builds wait for the current one.
<Details summary="Why is my deploy queued?">
    Free plans run one build at a time; later builds wait for the current one.
</Details>

Slots: root body summary

PropTypeRequiredDefaultDescription
summarycontentyesAlways-visible line that toggles the detail.
childrencontentDetail content.
openbooleanStart expanded.

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.

Filtering #

Filter bars, active-filter chips and result counts.

FilterBar #

Horizontal toolbar for filters and chips.

marketing-site #128

docs #127

marketing-site #126

api #125

function Example() {
    const query = signal("");
    const deploys = ["marketing-site #128", "docs #127", "marketing-site #126", "api #125"];
    return (
        <Stack gap="0.5rem">
            <FilterBar>
                <SearchInput
                    aria-label="Search deploys"
                    placeholder="Search deploys"
                    value={query()}
                    onChange={query.set}
                />
                <Button variant="outline" onClick={() => query.set("")}>
                    Clear
                </Button>
            </FilterBar>
            <For each={deploys.filter((d) => d.includes(query()))}>{(d) => <Text>{d}</Text>}</For>
        </Stack>
    );
}
PropTypeRequiredDefaultDescription
childrencontentFilter controls: search, selects, chips, buttons.

FilterChip #

Active filter with a remove button.

Status: failedBranch: mainAuthor: ada
function Example() {
    const filters = signal(["Status: failed", "Branch: main", "Author: ada"]);
    return (
        <Group gap="0.5rem">
            <For each={filters()}>
                {(filter) => (
                    <FilterChip
                        label={filter}
                        onRemove={() => filters.set(filters().filter((f) => f !== filter))}
                    />
                )}
            </For>
            <Show when={filters().length < 3}>
                <Button
                    size="sm"
                    variant="ghost"
                    onClick={() => filters.set(["Status: failed", "Branch: main", "Author: ada"])}
                >
                    Reset filters
                </Button>
            </Show>
        </Group>
    );
}

Slots: root remove

PropTypeRequiredDefaultDescription
labelstringyesFilter text, e.g. "Status: failed".
onRemove() => voidShows a remove (×) button; called when it is pressed.

ResultCount #

"12 results" line (live region).

1284 deploys

<ResultCount count={1284} label="deploys" />
PropTypeRequiredDefaultDescription
countnumberyesNumber of results.
labelstringNoun after the count, e.g. "deploys".

Also accepts the shared props: pass-through attributes, class, style, classes, styles, unstyled.