Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Data Table Parts

The sort header, selection cells, skeleton rows, empty row, column rules and hooks that DataList and DataGrid are both built from.

Status
stable
Since
0.7.0
Accessibility pattern
native table semantics

Last updated

RefChannelSize
r-1041Design tokensstable48 kB
r-1043Docs bundlestable1.2 MB
r-1042Registry manifestbeta12 kB

Install

Add Data Table Parts from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/data-table-parts

The same command installs the registry items it composes: @vegastack/button, @vegastack/checkbox, @vegastack/empty, @vegastack/skeleton, @vegastack/table.

Usage

import {
  SelectAllHead,
  SelectionCell,
  SortableHead,
  columnCellClass,
  cycleSort,
  useRowSelection,
} from "@/components/ui/data-table-parts";
import {
  Table,
  TableBody,
  TableCell,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

const { selected, allSelected, indeterminate, toggleAll, toggleRow } =
  useRowSelection({
    rowIds: rows.map((row) => row.id),
  });

<Table scrollLabel="Releases">
  <TableHeader>
    <TableRow>
      <SelectAllHead
        checked={allSelected}
        indeterminate={indeterminate}
        onToggle={toggleAll}
      />
      <SortableHead
        column={{ key: "name", header: "Release", sortable: true }}
        onSort={sortByName}
      />
    </TableRow>
  </TableHeader>
  <TableBody>
    {rows.map((row, index) => (
      <TableRow key={row.id}>
        <SelectionCell
          checked={selected.has(row.id)}
          onToggle={() => toggleRow(row.id)}
          label={`Select row ${index + 1}`}
        />
        <TableCell>{row.name}</TableCell>
      </TableRow>
    ))}
  </TableBody>
</Table>;

Scope

These are parts, not a table. They render chrome and compute set arithmetic; nothing here owns data, fetching, filtering or paging. Pick the renderer first — DataList for the presentational default, DataGrid when grouping, inline editing, multi-key sort, column management or 10k+ rows earn its engines — and reach for these directly only when you are building a table neither one fits.

The doctrinal split between the two renderers is unchanged: DataList stays presentational, DataGrid keeps its engines. What they share is chrome and set maths, which is what lives here.

Anatomy

PartWhat it is
SortableHeadOne header cell, with the sort affordance and the ARIA the sort state owes AT (aria-sort, data-sortable, data-sorted).
SortHeaderButtonThe control inside a sortable header — a ghost Button, so it inherits the system's hover, pressed and focus steps.
SelectAllHeadThe leading header cell holding the tri-state select-all checkbox.
SelectionCellThe leading body cell holding one row's checkbox. Stops mouse propagation so selecting never activates the row.
SkeletonRowsThe loading state, aria-hidden so a wall of placeholders is not announced.
EmptyRowThe "no records" state as a real full-width row, so the table keeps valid semantics.
useRowSelectionControlled-optional selection arithmetic, scoped to the current view.
useControlledStateThe house controlled-optional state idiom, once.
cycleSortasc → desc → cleared, with optional additive multi-key sorting capped at maxKeys.
columnCellClassAlignment, wrap posture and the mono numeral face for one column.
EmptyRow
SelectAllHead — data-slot="data-table-select-all"
SelectionCell — data-slot="data-table-selection-cell"
SkeletonRows
SortableHead
SortHeaderButton — data-slot="data-table-sort"

Examples

Sorting, selection and the column rules composed onto a plain Table:

RefChannelSize
r-1041Design tokensstable48 kB
r-1043Docs bundlestable1.2 MB
r-1042Registry manifestbeta12 kB

Loading and empty

RefReleaseChannelSize
RefReleaseChannelSize

No data

There are no records to display.

Column rules

columnCellClass resolves three facts at once, so header, body and skeleton cells cannot disagree:

  • alignstart (default), center, end.
  • mono — the mono numeral face (font-mono text-code tabular-nums), so figures line up.
  • nowrap — whether the cell stays on one line. Cells wrap by default; align="end" and mono columns opt in automatically, and an explicit nowrap overrides the inference in both directions.

API Reference

PropTypeDefaultDescription
column*DataTableHeaderColumnThe column this header cell describes.
directionSortDirectionnullDirection this column is currently sorted in, or null when it is not part of the active sort.
onSort((event: React.MouseEvent<HTMLButtonElement>) => void)Activate the sort. Omitted (or with column.sortable false) the header renders as static text.
ordernumber1-based priority within a multi-key sort. Omit for a single-key sort.

Data attributes and CSS variables on SortableHead

AttributeValues
data-sortable""
data-sortedmirrors a prop or state value
PropTypeDefaultDescription
key*stringStable identifier — the React key, the sort key, the visibility key.
align"center" | "end" | "start""start"Horizontal alignment of the header and cells.
monobooleanfalseRender this column's values in the mono numeral face (text-code + tabular-nums), so figures line up down the column.
nowrapbooleantrue for `align="end"` and `mono` columns, false otherwiseKeep this column's cells on one line instead of wrapping. Cells wrap by default (D18) — scrolling is reserved for tables that are genuinely wide, not forced by one long value. Figures and mono values are the exception and opt IN automatically.
PropTypeDefaultDescription
checked*booleanThis row is selected.
label*stringAccessible name for the checkbox — name the row, not the column.
onToggle*() => voidToggle this row.

Data attributes and CSS variables on SelectionCell

AttributeValues
data-slot"data-table-selection-cell"
PropTypeDefaultDescription
columns*DataTableColumnLayout[]The columns being rendered, so the placeholder matches the real geometry.
rowsnumber5How many placeholder rows to draw.
selectablebooleanfalseDraw the leading selection column's placeholder.
slotstring"data-table-skeleton-row"data-slot for each placeholder row, so each renderer keeps its own selector surface.

Data attributes and CSS variables on SkeletonRows

AttributeValues
data-slotmirrors a prop or state value

Accessibility

  • SortableHead emits aria-sort on every sortable column, "none" included — a table that marks only the active column tells a screen-reader user which column is sorted but never which ones they could sort.
  • SortHeaderButton is a real Button, so it is reachable, activatable and focus-visible without a single hand-rolled rule.
  • SelectAllHead is tri-state: partial selection reports indeterminate rather than a misleading checked or unchecked box.
  • Every selection checkbox needs a name that identifies its ROW (Select row 3, Select Acme), not the column.
  • SkeletonRows is aria-hidden; announce loading once from the table's own busy/live wiring instead of announcing every placeholder.
KeyAction
TabMove to the next sort header or selection checkbox.
Enter / SpaceSort by the focused column, or toggle the focused row.
Shift + clickAdd the column as an additional sort key (when additive).
ContractStates tested
Behaviourdefault, sorted, multi-sorted, checked, indeterminate, disabled, loading, empty
Accessibilitylabeled, keyboard, semantic-html
Visualdefault, hover, sorted-header, loading, empty

Do / Don't

Do
Compose these onto Table when neither DataList nor DataGrid fits, and let columnCellClass decide alignment, wrapping and the mono face.
Don't
Reimplement a sort header or selection cell inline — that is the duplication this item exists to end.

On this page