Table

A React data table with sorting, row selection and a sticky header - for admin panels and record lists.

Table playground

ChannelStatus
React 19 Release HighlightsTwitterPublished4,230
Design Tokens ArchitectureLinkedInPublished1,890
WAAPI Motion Deep DiveTwitterScheduled—
Q3 Product Roadmap PreviewNewsletterDraft—
Closed Token Vocabulary, ExplainedLinkedInPublished3,110
Shipping the Emoji PickerTwitterScheduled—
Sort by Post Title or Impressions - the header carets are real buttons.

Install Table

1. New project? One command installs the package and wires everything - see Installation:

bash
pnpm dlx zyncat-ui init

2. Import the component - it loads its own stylesheet:

tsx
import { Table } from '@zyncat/ui/table';

Usage

tsx
import { Table } from '@zyncat/ui/table';
 
<Table rows={rows} columns={columns} rowKey="id" selectable />

Compose Table in your application. No Tailwind or external styling library is required; every value resolves from Zyncat UI's token vocabulary.

Table props

Table

PropTypeDefaultDescription
columns*TableColumn<Row>[]—Column defs, rendered left to right.
rows*Row[]—The row data; sorted locally per the active sort before render.
rowKeystring—Row identity property. Default 'id'. Must be unique and stable.
ariaLabelstring—Accessible name for the <table>.
selectableboolean—Checkbox column + bulk bar. Selection is a Set of row keys, owned here.
onSelectionChange(keys: Array<string | number>) => void—Fires with the selected row keys after every selection change.
bulkActions(keys: Array<string | number>, clear: () => void) => ReactNode—Rendered in the bulk bar between the count and the built-in Clear.
selectionLabel(row: Row) => string—Per-row checkbox aria-label. Default 'Select row'.
defaultSortTableSort | null—Initial sort; sorting stays local and uncontrolled after that.
onSortChange(sort: TableSort) => void—Notification only - fires after the local sort updates.
density'cozy' | 'compact'—Row rhythm. Default 'cozy' (46px rows); 'compact' is 38px.
pinFirstboolean—Pin checkbox + first column under horizontal overflow. Default true.
loadingboolean—Rows recede and go inert (list-fetch convention).
emptyReactNode—Shown when rows is empty and not loading. Default 'Nothing to show'.
footerReactNode—Footer strip - pagination, summaries.
onRowClick(row: Row) => void—Makes rows clickable (checkbox cell excluded).
activateOnActivateOn'pointerdown'Whether a sortable header fires on `pointerdown` (snappier) or waits for `click`.
classNamestring—Size the table here - the internal scroller absorbs the constraint.
...htmlAttributesHTMLAttributes—Standard <div> attributes (style, data-*, aria-*, ...) forwarded to the table wrapper.

TableColumn

PropTypeDefaultDescription
key*string—Unique column id; also the default data accessor (row[key]).
labelReactNode—Header cell content; omit for a header-less column (e.g. an actions column).
alignTableAlign—Cell + header alignment. Default 'start'.
monoboolean—Mono + tabular numerals (timestamps, counts, IDs - section E).
strongboolean—Identity emphasis: strong text, medium weight.
growboolean—The one greedy column - absorbs slack width. Others size to content.
sortableboolean—Header becomes a sort control. Sort is local; asc - desc, no third state.
sortBystring | ((row: Row) => unknown)—Sort accessor: a row property name, or a function. Defaults to row[key].
hideBelowTableHideBelow—Collapse this column below a container breakpoint (30rem / 42rem).
render(row: Row) => ReactNode—Custom cell renderer. Defaults to row[key].

TableSort

PropTypeDefaultDescription
key*string—`key` of the column being sorted by.
dir*'asc' | 'desc'—Sort direction - `asc` or `desc`.

Frequently asked questions

Set sortable: true on a column - clicking its header (or activateOn="click" instead of the pointerdown default) cycles asc to desc, sorting a copy of rows locally with a numeric-aware compare. Pass defaultSort={{ key, dir }} to start pre-sorted, or read onSortChange to sync the active sort elsewhere; rows re-flow with a FLIP animation, not a jump cut.

Set selectable - it adds a checkbox column plus a bulk-action bar that slides in from the header once anything is checked. Selection is a Set reported through onSelectionChange, shift-click extends the range across rows, and bulkActions(keys, clear) renders your own buttons next to the built-in Clear.

Two mechanisms, no viewport media queries: hideBelow="sm" or "md" drops a column once the table's own container - not the window - crosses a 30rem or 42rem @container breakpoint, and whatever's left scrolls horizontally with a pinned identity column (pinFirst, on by default) and a fading edge that signals there's more to scroll.

No - nothing here is virtualized or cell-editable; it's a presentational table for columns, rows and a render function, not a spreadsheet engine. AG Grid and MUI X's Data Grid solve windowing and inline editing at enterprise scale; this solves "declare columns and rows, get sorting, selection, a sticky header and overflow handling for free."

Pass columns and rows: each column is a key (also the default row[key] accessor), an optional label, and an optional render(row) for a custom cell - <Table columns={columns} rows={rows} rowKey="id" />. mono gives a column tabular numerals for IDs and timestamps, strong adds identity emphasis, and align or grow control text alignment and which column absorbs the slack width.

Yes to both. It ships with 'use client' intact for the Next.js App Router, renders a real table/thead/tbody with scope="col" headers and aria-sort on the active sort column, and has zero runtime dependencies. A loading table fades its rows to 45% opacity and sets aria-busy; the row-reorder animation collapses to 1ms under prefers-reduced-motion.

Set in Geist & Newsreader — animated by the house engineZyncat UI · Rev 0.11 · MIT · Built by Tabsir Ahammed · Source on GitHub