opsinjs
ComponentsData display

Table

Rows and columns in a real semantic table, the readable twin a chart ships beside it, with a required caption, right-aligned numeric columns and its own scroll.

Preview

Theme
Density
Text
Status
table · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/table

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { Table } from "@/components/ui/table"
<Table
  caption="Example figures by period, and not a real record"
  columns={[
    { key: "period", header: "Period" },
    { key: "count", header: "Count", numeric: true },
  ]}
  rows={[
    { period: "Period one", count: "12" },
    { period: "Period two", count: "1,000" },
  ]}
/>

When to use it

Use it when

  • The accessible twin of a chart: the same figures a screen reader user can read.
  • A small set of rows and columns a reader compares directly.
  • A comparison read down a column, where lining the digits up is the point.

Do not use it when

  • One reading with a label, a time and a surface of its own.Use metric-tile instead.
  • A trend read as a shape rather than as figures.Use trend-sparkline instead.
  • The data has not arrived and you are holding the layout open.Use skeleton instead.
  • There are no rows, because the result is genuinely empty.Use empty-state instead.
  • An interactive grid with sorting, selection and virtualised scrolling.Use card instead.

Anatomy

  1. TableThe focusable scroll container and the table it wraps.Controlled by className
  2. Table.CaptionThe required caption. captionHidden hides it on screen only.Controlled by caption
  3. Table.HeadOne th per column, scope col, muted to read as labels.Controlled by columns
  4. Table.RowOne row per entry, hairline between. No hover or selected fill.Controlled by rows
  5. Table.CellOne cell per column. A numeric column right-aligns.Controlled by columns
  • Table 1Focusable, scrolls sideways
    • Table.Caption 1sr-only when captionHidden, never absent
    • Table.Head 1Header cells, scope col
    • Table.Body 1A tbody
      • Table.Row 1..nOne per row
        • Table.Cell 1..nFirst a th, the rest td

Examples

The readable twin of a chart

The chart draws the shape; the table carries the figures. captionHidden hides the caption the heading above already supplies.

Theme
Density
Text
Status
table-a-chart-twin · base base · style base-lyraOpen under the product theme

Numeric columns line up on their digits

numeric right-aligns two columns in tabular figures while the first column labels each row.

Theme
Density
Text
Status
table-numeric-columns · base base · style base-lyraOpen under the product theme

Content guidelines

Write a caption naming the rows and columns, so the table stands on its own. Where a heading already names the data, keep the caption and set captionHidden.

Do

"Weekly figures for the example account, in the example unit" names rows and columns.

Don’t

"Table", or an empty caption, leaves numbers counting nothing.

Accessibility

Audited against WCAG 2.2 AA in a source pass and a rendered pass on /view/base/base-lyra/component/table. The audit is author-run, not independent, and clinical review is pending.

The audit found nothing to change in the source. The rendered pass is clean: no axe violations, no target-size failures, no focus-visible gaps, no sideways page scroll at 320px. A real <caption> always renders, headers carry scope="col", and the first body cell carries scope="row" when rowHeader is on. The overflow-x-auto container is tabIndex=0, reachable and scrollable by keyboard. Padding-based spacing and no fixed cell heights keep text-spacing overrides from clipping. No colour prop, no data-status, no data-category, no transition.

Two items remain open. An empty caption renders an empty <caption>, warning only in development. The scroll container has no role and no accessible name; adding role="region" would require one, left to the consuming page.

Contrast for the header ink, caption ink and hairlines has not been measured; the report below stands in its place.

KeyActionNotes
TabMoves focus onto the scrollable table, then past itPresent even when the table fits.
Shift+TabMoves back out of the tableCells are never focus stops.
Left, RightScroll the table sideways when the container has focusNative to a focusable scroll region.
SpaceScrolls the page, as anywhere elseNo control here captures it.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

Data attributes

AttributeConditionValue
data-slotOn the container, the table and every inner part, alwaystable-container, table, table-caption, table-header, table-head, table-body, table-row, table-cell
data-opsinjs-valueOn a td in a numeric columnThe empty string, deliberately: a tabular-figures hook, not a magnitude

API reference

Prop

Type

Generated from TableProps in registry/bases/base/table.tsx.

caption is a required string. rowHeader defaults to true, so the first cell of each row is a th. There is no onSort, no selectedRows and no virtualized.

  • MetricTile is one labelled reading on its own surface.
  • TrendSparkline draws a series as a shape.
  • Card is a surface for one record laid out freely.
  • ResultCard places one result with its explanation.

On this page