Table A table lists records that share the same fields: members, keys, invites, audit events. Under 640px each row stacks into a card with its column header beside each value.
Preview apps/web/components/ui/KbTable.tsx
Examples Usage Code Accessibility
Columns, numbers and row actions Numbers are right-aligned with tabular figures. Row actions sit in the last column.
Team members Name Role Open requests Actions Ana Ruiz Owner 12 Remove Dev Patel Agent 4 Remove
Show code <KbTable caption="Team members" rows={rows} rowKey={(r) => r.id} columns={columns}
rowActions={(r) => <KbButton variant="ghost" size="sm" aria-label={`Remove ${r.name}`}>Remove</KbButton>} />Identity cell and row open The first column can hold an avatar, a name and a quiet line. With rowHref or onRowOpen the first cell becomes the real link or button and its hit area covers the row.
Show code <KbTable caption="Contacts" rows={rows} rowKey={(r) => r.id} rowHref={(r) => `/admin/contacts/${r.id}`} rowOpenLabel={(r) => `Open ${r.name}`}
columns={[{ key: "who", header: "Name", cell: (r) => <KbIdentityCell name={r.name} sub={r.role} /> }]} />Empty No pending invites. Invite a teammate above.
Show code <KbTable caption="Pending invites" rows={[]} rowKey={(r) => r.id} columns={columns}
empty="No pending invites. Invite a teammate above." />When to use Use a table when each record has three or more comparable fields. For one or two, use a list. Keep headers short nouns in sentence case; they are also the labels when the table stacks. Give each row action a unique name, such as Revoke invite for ana@acme.com. Do not put controls in a header. Edit a complex row on its own page or in a dialog. Keys Name Role Ana Ruiz Owner
DoShow one fact per column, with the status as a lozenge. Keys Details Ana Ruiz, Owner, 12 open
Don'tPack several facts into one cell. Import import { KbTable } from "@/components/ui/KbTable";
<KbTable caption="Team members" rows={members} rowKey={(m) => m.id}
columns={[{ key: "name", header: "Name", cell: (m) => m.name }]} />Props KbTable Prop Type Default What it does captionstringNone Required. Names the table for screen readers; not shown. columnsKbTableColumn[]None key, header, cell(row), and optional numeric or mono. rows / rowKeyRow[] / (row) => stringNone The data and a stable key for each row. rowActions(row) => ReactNodeNone Buttons for one row, in the last column. rowMuted(row) => booleanNone Quiets a row, for example a revoked key. emptyReactNodeNone Shown in place of the table when there are no rows. rowHref / onRowOpen(row) => string / (row) => voidNone Makes the first cell a real link or button covering the row. Keep that cell free of other controls. rowOpenLabel, rowSelected(row) => string, (row) => booleanNone Accessible name for the opener; marks the open row with aria-current.
KbIdentityCell Prop Type Default What it does namestringNone The person or record's name. substringNone One quiet line: email, role. avatarUrlstring | nullNone Picture; initials show without it. Decorative.
Keyboard and screen readers A real table with a caption, column headers with scope=col, and a hidden header for the actions column. When stacked, each value keeps its column header as a visible label. Status is a lozenge with words; a muted row is never the only cue. Row actions need unique accessible names.