Table
Semantic data-table primitives that render real table/th scope/aria-sort semantics on Web.
Identity
Section titled “Identity”- Category: Data display
- Status: stable public Registry/export-map component family
- Targets: iOS · Android · Web, subject to the compatibility contract
- Prerequisites:
@beemvp/beeui-uiinstalled (or this component’s source copied via the Registry CLI below) and, on Web, the BeeUI Tailwind/Uniwind theme CSS loaded — see Start for full platform setup. - Source:
packages/ui/src/components/table.tsx
Import
Section titled “Import”import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from '@beemvp/beeui-ui';There is no documented deep/private source import. For source ownership from a BeeUI checkout:
pnpm beeui add tableRegistry (used throughout this page) is BeeUI’s source-ownership manifest — registry/registry.json — that the pnpm beeui/@beemvp/beeui-cli CLI reads to copy a component’s real source into your app and rewrite its internal imports; it is not an npm package index. This component’s own Registry entry: registry/registry.json.
Composition and public API
Section titled “Composition and public API”- Family exports:
TableTableBodyTableCaptionTableCellTableFooterTableHeadTableHeaderTableRow- Also routed here, outside the Registry family:
table
- Package export subpath:
@beemvp/beeui-ui/table
- Also routed here, outside the Registry family:
Exported types: TableAlign, TableBodyProps, TableCaptionProps, TableCellProps, TableDensity, TableFooterProps, TableHeaderProps, TableHeadProps, TableLayout, TableProps, TableRowProps, TableSortDirection
The generated API inventory is mechanically joined to packages/ui/src/index.ts, Registry metadata, and the component reference contract. Each type’s field table below is parsed directly from that source, not a second hand-maintained copy; for the fuller behavior narrative see the canonical component behavior catalog.
State and behavior contract
Section titled “State and behavior contract”Composable primitive family with no owned fetching, sort/filter/selection state, or spreadsheet-style cell navigation. TableHead’s sortDirection/onSortChange pair is fully caller-controlled (the presence of sortDirection is what marks a column sortable); TableRow’s selected is a caller-owned boolean reflected only visually/for accessibility, and its optional onPress makes the whole row a real keyboard-reachable pressable. layout ('scroll' default, or 'stacked') is an explicit caller choice — Table never measures viewport width itself. TableHead/TableCell accept an align prop ('start' | 'center' | 'end') for numeric/action columns instead of the platform-specific className="items-end text-end" combination. Table’s density overrides the row height for that one table only ('compact'/'comfortable'/'spacious' reuse the global density-axis values; 'dense48' is a fourth, table-specific 48px step) and is ignored in layout="stacked"; omitting it leaves rows following the ambient global density exactly as before.
TableBodyProps
Section titled “TableBodyProps”| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode |
— | Content rendered inside this element. The family’s composition section states which children it expects. |
className |
string |
— | Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee. |
Also carries every prop of Omit<ViewProps, 'children'> — that upstream contract is not reproduced here.
Platform differences (native vs. Web):
testIDis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.- Base type differs: native carries
Omit<ViewProps, 'children'>; Web carriesOmit<React.HTMLAttributes<HTMLElement>, 'children'>.
TableCaptionProps
Section titled “TableCaptionProps”| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode |
— | Content rendered inside this element. The family’s composition section states which children it expects. |
className |
string |
— | Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee. |
Also carries every prop of Omit<React.ComponentProps<typeof Text>, 'children'> — that upstream contract is not reproduced here.
Platform differences (native vs. Web):
testIDis declared explicitly on Web — on native it may come fromOmit<React.ComponentProps<typeof Text>, 'children'>, which this table does not reproduce.- Base type differs: native carries
Omit<React.ComponentProps<typeof Text>, 'children'>; Web carriesOmit<React.HTMLAttributes<HTMLElement>, 'children'>.
TableCellProps
Section titled “TableCellProps”| Prop | Type | Default | Description |
|---|---|---|---|
align |
TableAlign |
— | Cell content alignment — see table-shared.ts’s TableAlign. Defaults to 'start' in layout="scroll" and 'end' in layout="stacked" (the value column’s long-standing default, opposite its label — preserved so existing layout="stacked" usage renders unchanged). |
children |
React.ReactNode |
— | Content rendered inside this element. The family’s composition section states which children it expects. |
className |
string |
— | Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee. |
colSpan |
number |
1 |
Number of columns this cell spans (e.g. a single full-width cell used for an empty/loading/error row — see Table’s composition notes). Native has no table-layout engine, so a spanning cell approximates width by growing its flex share proportionally (flex: colSpan) rather than measuring sibling column widths. |
label |
string |
— | Explicit column label override for layout="stacked" and native accessible-name column context. Falls back to the corresponding TableHead’s inferred label when omitted. |
Also carries every prop of Omit<ViewProps, 'children'> — that upstream contract is not reproduced here.
Platform differences (native vs. Web):
colSpanis declared explicitly on native (native default1) — on Web it may come fromOmit<React.TdHTMLAttributes<HTMLElement>, 'align' | 'children'>, which this table does not reproduce.accessibilityLabelis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.accessibilityLabelledByis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.testIDis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.- Base type differs: native carries
Omit<ViewProps, 'children'>; Web carriesOmit<React.TdHTMLAttributes<HTMLElement>, 'align' | 'children'>.
TableFooterProps
Section titled “TableFooterProps”| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode |
— | Content rendered inside this element. The family’s composition section states which children it expects. |
className |
string |
— | Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee. |
Also carries every prop of Omit<ViewProps, 'children'> — that upstream contract is not reproduced here.
Platform differences (native vs. Web):
testIDis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.- Base type differs: native carries
Omit<ViewProps, 'children'>; Web carriesOmit<React.HTMLAttributes<HTMLElement>, 'children'>.
TableHeaderProps
Section titled “TableHeaderProps”| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode |
— | Content rendered inside this element. The family’s composition section states which children it expects. |
className |
string |
— | Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee. |
Also carries every prop of Omit<ViewProps, 'children'> — that upstream contract is not reproduced here.
Platform differences (native vs. Web):
testIDis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.- Base type differs: native carries
Omit<ViewProps, 'children'>; Web carriesOmit<React.HTMLAttributes<HTMLElement>, 'children'>.
TableHeadProps
Section titled “TableHeadProps”| Prop | Type | Default | Description |
|---|---|---|---|
align |
TableAlign |
'start' |
Header content alignment — see table-shared.ts’s TableAlign. Defaults to 'start'. |
children |
React.ReactNode |
— | Content rendered inside this element. The family’s composition section states which children it expects. |
className |
string |
— | Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee. |
label |
string |
— | Explicit column label override. Required when this header’s content is not plain text/number (e.g. an icon-only header) — inferred from children otherwise. Drives both layout="stacked"’s visible label-value pairing and the column context folded into each native TableCell’s accessible name (RN has no dedicated table/column-header accessibility role to rely on instead — ADR-007). |
onSortChange |
() => void |
— | Caller-driven sort-toggle callback. Table stores no sort state. |
sortDirection |
TableSortDirection |
— | Controlled current sort state for this column. Presence of this prop (any of the three values) marks the column sortable and renders an interactive sort trigger reachable by normal tab order. |
Also carries every prop of Omit<ViewProps, 'children'> — that upstream contract is not reproduced here.
Platform differences (native vs. Web):
testIDis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.- Base type differs: native carries
Omit<ViewProps, 'children'>; Web carriesOmit<React.ThHTMLAttributes<HTMLElement>, 'align' | 'children' | 'scope'>.
TableProps
Section titled “TableProps”| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode |
— | Content rendered inside this element. The family’s composition section states which children it expects. |
className |
string |
— | Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee. |
density |
TableDensity |
— | Per-table row-height override for layout="scroll" rows, replacing the ambient global application-density row height for this one Table only — see TableDensity. Omitted (the default) leaves every row following the global density exactly as before this prop existed; existing tables are unaffected. |
layout |
TableLayout |
'scroll' |
Responsive presentation. Defaults to 'scroll' (horizontal ScrollView around the row grid). Set 'stacked' to render a card/label-value presentation instead — typically driven by the caller’s own breakpoint decision (BeeUI does not own viewport/breakpoint policy). |
Also carries every prop of Omit<ViewProps, 'children'> — that upstream contract is not reproduced here.
Platform differences (native vs. Web):
accessibilityLabelis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.accessibilityLabelledByis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.testIDis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.- Base type differs: native carries
Omit<ViewProps, 'children'>; Web carriesOmit<React.HTMLAttributes<HTMLDivElement>, 'children'>.
TableRowProps
Section titled “TableRowProps”| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode |
— | Content rendered inside this element. The family’s composition section states which children it expects. |
className |
string |
— | Extra utility classes, merged after the component’s own via cn(...), so they win on conflict. An escape hatch for source-owned and application work, not a cross-engine portability guarantee. |
onPress |
() => void |
— | Makes the row itself pressable (e.g. a row-to-detail navigation pattern: onPress={() => router.push(...)}), mirroring ListItem’s own opt-in onPress contract. Renders the row as a Pressable with accessibilityRole="button" (keyboard Enter/Space activate it on Web through RN’s own Pressable-on-Web keyboard handling) instead of a plain View — a row with no onPress keeps rendering exactly as before. |
selected |
boolean |
false |
Visual highlight for a caller-selected row. Table owns no selection state (ADR-007) — this only reflects a boolean the caller already tracks (e.g. alongside a Checkbox in one of the row’s cells). |
Also carries every prop of Omit<ViewProps, 'children'> — that upstream contract is not reproduced here.
Platform differences (native vs. Web):
accessibilityLabelis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.accessibilityLabelledByis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.testIDis declared explicitly on Web — on native it may come fromOmit<ViewProps, 'children'>, which this table does not reproduce.- Base type differs: native carries
Omit<ViewProps, 'children'>; Web carriesOmit<React.HTMLAttributes<HTMLElement>, 'children'>.
Related exported types:
TableAlign— one of'start','center','end'.TableDensity— one of'compact','comfortable','spacious','dense48'.TableLayout— one of'scroll','stacked'.TableSortDirection— one of'ascending','descending','none'.
The executable fixtures below are the source-grounded usage examples; consumers should not infer state ownership from DOM structure or another UI library.
Provider and dependencies
Section titled “Provider and dependencies”- No additional provider is required by this family.
BeeUIProviderremains the recommended application root. - Peer/native dependencies visible to this Registry item:
react,react-native - Registry dependency closure:
core-cn,text,theme,use-direction,use-required-callback-warning - Safe-area ownership remains explicit: shell surfaces touching system edges opt into
SafeArea; components do not silently invent app-shell insets. - Web consumers load the BeeUI semantic theme CSS as documented in Web onboarding.
Platform behavior
Section titled “Platform behavior”This family is split by platform and renders from table.web.tsx (Web). Where the two shapes differ, the difference is listed under the affected type above rather than summarised here.
This family has platform-split source files. The bundler selects the native/Web implementation; do not infer native runtime behavior from the Web preview.
Evidence classes are not equal and this page does not blur them: Web behavior is exercised in a real browser, while iOS and Android carry package/export and native-compile evidence, which is not device-runtime proof. The compatibility contract records which class each claim rests on.
Accessibility
Section titled “Accessibility”- Roles this family assigns:
button(iOS and Android),cell(Web),row(Web),rowgroup(Web),table(Web) — set intable.tsx,table.web.tsxby the components themselves, not by the caller. - Accessibility states and properties it sets:
accessibilityLabel,accessibilityLabelledBy(Web),accessible(iOS and Android),hidden(Web),label(Web),labelledby(Web),selected(Web),sort(Web) — read fromtable.tsx,table.web.tsx.
Keyboard/focus behavior, announcements, Dynamic Type/Web zoom, RTL and reduced-motion expectations are not derived here — see Accessibility overview, Keyboard & focus, RTL/localization and Large text & zoom. BeeUI does not claim universal accessibility certification from automated tests.
Styling and theming
Section titled “Styling and theming”- Style axes:
align(3 values). - Class-name surfaces:
className.
Colors, spacing and typography come from semantic tokens rather than from values written here — see Theming and Density. A className is an escape hatch for source-owned and application work, not a cross-engine portability guarantee.
Executable examples
Section titled “Executable examples”- Primary executable fixture:
apps/showcase/__tests__/dynamic-type-contract.test.tsx - Additional fixture:
apps/showcase/__tests__/perf-render-commit.test.tsx - Additional fixture:
apps/showcase/__tests__/table-cell-alignment-and-content-hug.test.tsx - Additional fixture:
apps/showcase/__tests__/table-mixed-text-children.test.tsx
Addressable examples
Section titled “Addressable examples”Each link below opens the Showcase at that exact example, not at the top of the gallery:
The Showcase links demonstrate Web behavior; use the native-preview guide for real simulator/emulator/device paths.
Live Web preview
Section titled “Live Web preview”This frame loads the real BeeUI Web Showcase on demand; it is not a second docs-only implementation. It proves browser behavior only. Use native preview for iOS/Android simulator, emulator or device paths.
Composition anatomy
Section titled “Composition anatomy”- Family root / primary export:
Table- Public composition parts / helpers:
TableBodyTableCaptionTableCellTableFooterTableHeadTableHeaderTableRow
- Exported type surface:
TableAlignTableBodyPropsTableCaptionPropsTableCellPropsTableDensityTableFooterPropsTableHeaderPropsTableHeadPropsTableLayoutTablePropsTableRowPropsTableSortDirection
- Public composition parts / helpers:
The tree above is ordinary document structure so it remains readable with keyboard and assistive technology; it is derived from the real public export family rather than a canvas-only diagram.
Verified example source
Section titled “Verified example source”The following is the exact typechecked runtime Showcase fixture selected for this live preview: apps/showcase/component-gallery/table-showcase.tsx. Runtime gallery/pattern sources are preferred over test harnesses, and the displayed source and executable source are the same file; there is no separately maintained demo snippet.
import { Badge, Card, Checkbox, IconButton, Section, Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, Text, VStack, type TableSortDirection,} from '@beemvp/beeui-ui';import * as React from 'react';
type TeamMember = { id: string; name: string; role: string; status: 'Active' | 'Invited';};
const TEAM_MEMBERS: TeamMember[] = [ { id: 'ada', name: 'Ada Lovelace', role: 'Engineering', status: 'Active' }, { id: 'grace', name: 'Grace Hopper', role: 'Engineering', status: 'Active' }, { id: 'alan', name: 'Alan Turing', role: 'Research', status: 'Invited' },];
// Real, caller-owned sort — proves `TableHead`'s `sortDirection`/`onSortChange`// contract drives actual row order rather than a purely decorative indicator// (ADR-007 "State boundaries": Table stores no sort state itself).function sortByName(rows: TeamMember[], direction: TableSortDirection): TeamMember[] { if (direction === 'none') return rows; const sorted = [...rows].sort((a, b) => a.name.localeCompare(b.name)); return direction === 'descending' ? sorted.reverse() : sorted;}
function nextSortDirection(current: TableSortDirection): TableSortDirection { if (current === 'none') return 'ascending'; if (current === 'ascending') return 'descending'; return 'none';}
function TeamTable({ layout }: { layout?: 'scroll' | 'stacked' }) { const [sortDirection, setSortDirection] = React.useState<TableSortDirection>('none'); const [selectedIds, setSelectedIds] = React.useState<Set<string>>(new Set());
const rows = React.useMemo(() => sortByName(TEAM_MEMBERS, sortDirection), [sortDirection]); const allSelected = selectedIds.size === TEAM_MEMBERS.length; const someSelected = selectedIds.size > 0 && !allSelected;
const setRowSelected = (id: string, selected: boolean) => { setSelectedIds((previous) => { const next = new Set(previous); if (selected) next.add(id); else next.delete(id); return next; }); };
return ( <VStack gap="sm"> <Table layout={layout}> <TableCaption>Team members</TableCaption> <TableHeader> <TableRow> <TableHead label="Select all"> <Checkbox accessibilityLabel="Select all team members" checked={allSelected ? true : someSelected ? 'indeterminate' : false} onCheckedChange={(checked) => setSelectedIds(checked ? new Set(TEAM_MEMBERS.map((member) => member.id)) : new Set()) } /> </TableHead> <TableHead onSortChange={() => setSortDirection(nextSortDirection(sortDirection))} sortDirection={sortDirection}> Name </TableHead> <TableHead>Role</TableHead> <TableHead>Status</TableHead> <TableHead label="Actions">Actions</TableHead> </TableRow> </TableHeader> <TableBody> {rows.map((member) => ( <TableRow key={member.id} selected={selectedIds.has(member.id)}> <TableCell label="Select"> <Checkbox accessibilityLabel={`Select ${member.name}`} checked={selectedIds.has(member.id)} onCheckedChange={(checked) => setRowSelected(member.id, checked)} /> </TableCell> <TableCell>{member.name}</TableCell> <TableCell>{member.role}</TableCell> <TableCell> <Badge variant={member.status === 'Active' ? 'success' : 'secondary'}> {member.status} </Badge> </TableCell> <TableCell label="Actions"> <IconButton accessibilityLabel={`Edit ${member.name}`} variant="ghost"> ✎ </IconButton> </TableCell> </TableRow> ))} </TableBody> </Table> <Text tone="muted" variant="caption"> {`Sort: ${sortDirection} · Selected: ${selectedIds.size}`} </Text> </VStack> );}
export function TableShowcase() { return ( <VStack gap="lg"> <Card className="gap-4" testID="table-showcase" variant="raised"> <Section description="Real HTML table/th-scope semantics, a keyboard-reachable sort trigger in normal tab order, and caller-owned row selection via Checkbox (ADR-007)." title="Sortable, selectable table" > <TeamTable /> </Section> </Card>
<Card className="gap-4" testID="table-showcase-stacked"> <Section description="Explicit opt-in card/label-value presentation for narrow viewports — the same composed rows, no duplicated `.map()` loop." title="Stacked layout" > <TeamTable layout="stacked" /> </Section> </Card> </VStack> );}Use the code block’s copy affordance to copy the exact fixture. For a smaller app-specific example, start from the public imports shown above and keep only the state your screen owns.
Limitations
Section titled “Limitations”colSpan is an approximation on native: with no table-layout engine a spanning cell grows its flex share instead of measuring the columns it covers. A TableHead whose content is not plain text needs an explicit label, because both the stacked layout’s visible pairing and each native cell’s accessible name are built from it. A row with onPress should not also embed its own interactive controls on native: the row renders as a single button-role Pressable, so a screen reader exposes the whole row as one button regardless of what its cells contain — prefer a single cell action or a leading Checkbox instead of a row-level onPress when a row needs its own actionable controls. Web keeps row and cell activation independent (a click or Enter/Space on an embedded interactive descendant activates that control, not the row), so this combination is onPress-only-on-Web territory, not a cross-platform pattern.
Implementation note: Platform-split; see docs/data-typography.md for data-cell typography. Row-to-detail navigation pattern: TableRow accepts an opt-in onPress (mirroring ListItem) to make an entire row navigate to a detail screen, e.g. <TableRow onPress={() => router.push(/orders/${order.id})}>. It renders as a real, keyboard-reachable pressable row (native: accessibilityRole="button"; Web: the row stays a real <tr>/role="row" — not role="button", which would break table semantics — and adds tabIndex={0} plus Enter/Space keydown activation alongside the pointer onClick) while every row without onPress keeps rendering exactly as before. Prefer this over wrapping a whole row in your own Pressable, which duplicates row semantics; for a row that only needs one actionable cell (not the whole row), put an IconButton in its own TableCell instead.