Skip to content

Table

Semantic data-table primitives that render real table/th scope/aria-sort semantics on Web.

  • Category: Data display
  • Status: stable public Registry/export-map component family
  • Targets: iOS · Android · Web, subject to the compatibility contract
  • Prerequisites: @beemvp/beeui-ui installed (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 { 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:

Terminal window
pnpm beeui add table

Registry (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.

  • Family exports: Table TableBody TableCaption TableCell TableFooter TableHead TableHeader TableRow
    • Also routed here, outside the Registry family:
      • table
    • Package export subpath: @beemvp/beeui-ui/table

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.

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.

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):

  • testID is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • Base type differs: native carries Omit<ViewProps, 'children'>; Web carries Omit<React.HTMLAttributes<HTMLElement>, 'children'>.
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):

  • testID is declared explicitly on Web — on native it may come from Omit<React.ComponentProps<typeof Text>, 'children'>, which this table does not reproduce.
  • Base type differs: native carries Omit<React.ComponentProps<typeof Text>, 'children'>; Web carries Omit<React.HTMLAttributes<HTMLElement>, 'children'>.
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):

  • colSpan is declared explicitly on native (native default 1) — on Web it may come from Omit<React.TdHTMLAttributes<HTMLElement>, 'align' | 'children'>, which this table does not reproduce.
  • accessibilityLabel is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • accessibilityLabelledBy is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • testID is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • Base type differs: native carries Omit<ViewProps, 'children'>; Web carries Omit<React.TdHTMLAttributes<HTMLElement>, 'align' | 'children'>.
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):

  • testID is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • Base type differs: native carries Omit<ViewProps, 'children'>; Web carries Omit<React.HTMLAttributes<HTMLElement>, 'children'>.
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):

  • testID is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • Base type differs: native carries Omit<ViewProps, 'children'>; Web carries Omit<React.HTMLAttributes<HTMLElement>, 'children'>.
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):

  • testID is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • Base type differs: native carries Omit<ViewProps, 'children'>; Web carries Omit<React.ThHTMLAttributes<HTMLElement>, 'align' | 'children' | 'scope'>.
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):

  • accessibilityLabel is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • accessibilityLabelledBy is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • testID is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • Base type differs: native carries Omit<ViewProps, 'children'>; Web carries Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>.
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):

  • accessibilityLabel is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • accessibilityLabelledBy is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • testID is declared explicitly on Web — on native it may come from Omit<ViewProps, 'children'>, which this table does not reproduce.
  • Base type differs: native carries Omit<ViewProps, 'children'>; Web carries Omit<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.

  • No additional provider is required by this family. BeeUIProvider remains 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.

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.

  • Roles this family assigns: button (iOS and Android), cell (Web), row (Web), rowgroup (Web), table (Web) — set in table.tsx, table.web.tsx by 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 from table.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.

  • 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.

Each link below opens the Showcase at that exact example, not at the top of the gallery:

  • basic — smallest meaningful complete usage
  • states — the states example

The Showcase links demonstrate Web behavior; use the native-preview guide for real simulator/emulator/device paths.

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.

  • Family root / primary export: Table
    • Public composition parts / helpers:
      • TableBody
      • TableCaption
      • TableCell
      • TableFooter
      • TableHead
      • TableHeader
      • TableRow
    • Exported type surface:
      • TableAlign
      • TableBodyProps
      • TableCaptionProps
      • TableCellProps
      • TableDensity
      • TableFooterProps
      • TableHeaderProps
      • TableHeadProps
      • TableLayout
      • TableProps
      • TableRowProps
      • TableSortDirection

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.

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.

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.