Skip to main content
BM

Table

A captioned data table that scrolls sideways inside itself when it's too wide.

View source on GitHub

Page views this month
PageViewsUpdated
Home1,204Oct 2026
Design System386Oct 2026
Components92Oct 2026
TableExample.tsx
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeaderCell,
  TableRow,
} from "@/components/Table";

const pages = [
  { page: "Home", views: 1204, updated: "Oct 2026" },
  { page: "Design System", views: 386, updated: "Oct 2026" },
  { page: "Components", views: 92, updated: "Oct 2026" },
];

export function TableExample() {
  return (
    <Table caption="Page views this month">
      <TableHead>
        <TableRow>
          <TableHeaderCell>Page</TableHeaderCell>
          <TableHeaderCell align="end">Views</TableHeaderCell>
          <TableHeaderCell>Updated</TableHeaderCell>
        </TableRow>
      </TableHead>
      <TableBody>
        {pages.map(({ page, views, updated }) => (
          <TableRow key={page}>
            <TableHeaderCell>{page}</TableHeaderCell>
            <TableCell align="end">{views.toLocaleString("en-US")}</TableCell>
            <TableCell>{updated}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

Examples

Basic

Keyboard-focusable scrollers
EngineBrowsersScrollers focusable by default
BlinkChrome, Edge, OperaYes, from Chrome 130, when nothing inside is focusable
GeckoFirefoxYes
WebKitSafariNo

Wide

Library keyboard support
ComponentTabShift+TabEnterSpaceArrow keysHome / EndEscapeA to Z
ButtonFocuses itFocuses itActivates itActivates it————
LinkFocuses itFocuses itFollows itScrolls the page————
MenuCloses, moves onCloses, moves backOpens; chooses an itemOpens; chooses or togglesOpens; next or previous itemFirst or last itemClosesNext matching item
AccordionNext heading or panelPrevious heading or panelOpens or closesOpens or closesScroll the page———
TooltipShows itShows it————Hides it—
CodeBlockCopy button, then the codeThe code, then the copy buttonCopiesCopiesScroll the focused code———
TableThe table, while it's too wideThe table, while it's too wide——Scroll the focused table———

Tables too wide for the panel scroll inside their box. While one scrolls, it's a Tab stop:

Announced as: “Library keyboard support, group”

Example build sizes
FileSize (kB)
RawGzip
index.html4.21.6
entry.client.js182.457.9
root.css21.34.8
Total207.964.3

Hidden caption

Theme keys

Theme keys
Stored valueThe page shows
lightThe Light theme, whatever the system uses
darkThe Dark theme, whatever the system uses

Announced as: “Theme keys, table”

Customized with component tokens

Keyboard-focusable scrollers
EngineBrowsersScrollers focusable by default
BlinkChrome, Edge, OperaYes, from Chrome 130, when nothing inside is focusable
GeckoFirefoxYes
WebKitSafariNo
CSS
.customTable {
  --component-table-header-background: var(--color-accent-background);
  --component-table-border: var(--color-accent-border);
}

Props

Any native prop not listed here goes to the root element unchanged.

TableHead, TableBody, TableFoot and TableRow take only children and their element's native props. className and native props reach each part's element, and Table's reach the root div. caption holds no links or buttons. Native thead, tr, th and td inside a Table get the look, but not the automatic scope.

Table props
PropTypeDefaultDescription
captionReactNode—Required. Names the table, and its focusable group while it scrolls. Shown above the panel unless hideCaption. No interactive content.
hideCaptionbooleanfalseHides the caption visually; it still names the table for assistive technology. Use it only when visible text right before the table already says the same thing.
childrenReactNode—Required. TableHead, TableBody and TableFoot (or native thead, tbody and tfoot), after any colgroup elements.
TableHeaderCell props
PropTypeDefaultDescription
scope"col" | "row" | "colgroup" | "rowgroup"—Which cells it heads. Unset: "col" in TableHead, "row" in TableBody or TableFoot, and none elsewhere. Set it for a column header in the body, or to "colgroup" for a header over a column group, which needs a matching <colgroup span> before the head.
align"start" | "center" | "end""start"Text alignment in the cell. Use "end" for numeric columns, so digits line up.
childrenReactNode—Required. The header text.
TableCell props
PropTypeDefaultDescription
align"start" | "center" | "end""start"Text alignment in the cell. Use "end" for numeric columns, so digits line up.
childrenReactNode—Required. The cell's content.

Component tokens

Table component tokens
TokenCopyDescriptionAliasesLightDarkPreview
--component-table-backgroundPanel fill, and the first body row and every other one after it.color.surface.raised#fffffffrom color.gray.0#18202efrom color.slate.900
--component-table-stripe-backgroundFill of the body rows between them (decorative: the table's semantics carry its rows).color.surface.base#fbf8f2from color.sand.50#0b1220from color.slate.950
--component-table-textCell text.color.text.default#17202bfrom color.gray.900#fffffffrom color.gray.0
--component-table-borderPanel border and the lines under the head and above the foot (decorative).color.border.subtle#e5dccbfrom color.sand.200#272f3ffrom color.slate.800
--component-table-border-widthLine thickness.effect.border.width2pxfrom effect.line.22pxfrom effect.line.2
--component-table-radiusPanel corner radius.radius.surface4pxfrom radius.44pxfrom radius.4
--component-table-header-backgroundFill of the head and foot rows.color.surface.sunken#f2ede2from color.sand.100#060a13from color.slate.1000
--component-table-header-font-weightColumn header weight, in the head.type.weight.strong600from type.font-weight.600600from type.font-weight.600
--component-table-row-header-font-weightRow header weight, in the body and foot.type.weight.body400from type.font-weight.400400from type.font-weight.400
--component-table-cell-padding-blockBlock padding of every cell.space.inset.sm0.5remfrom space.20.5remfrom space.2
--component-table-cell-padding-inlineInline padding of every cell.space.inset.sm0.5remfrom space.20.5remfrom space.2
--component-table-caption-colorVisible caption text, above the panel.color.text.muted#455464from color.gray.700#e6ecf3from color.slate.100
--component-table-caption-gapSpace between the caption and the panel.space.stack.sm0.5remfrom space.20.5remfrom space.2
--component-table-focus-ring-colorFocus indicator of an overflowing table, around the caption and the panel.color.focus.ring#005f7bfrom color.cyan.700#54d8f4from color.cyan.300

Accessibility

  • The caption names the table, and its focusable group while it scrolls. hideCaption hides it visually but keeps it for assistive technology.
  • A header cell's scope comes from where it sits: "col" in TableHead, "row" in TableBody and TableFoot. An explicit scope always wins.
  • While it's too wide to fit, the table is a focusable group named by its caption, so a keyboard can scroll it. Safari doesn't make scrolling boxes focusable on its own, and Chrome only does when nothing inside is focusable. Once it fits, it's no longer a Tab stop. Before the page's script runs, every table is one.
  • It's a group, not a region, so a page of tables doesn't fill the landmarks list.
  • The stripes, border and lines are decorative: the table's semantics carry its rows and columns. In forced colors the stripes go, and each body row gets a line instead.
  • Use a table for tabular data, never for layout.
Table keyboard
KeyAction
Tab / Shift+TabMoves to the table while it's too wide to fit, then to any controls in its cells.
← / →Scrolls the focused table sideways.