@zuilib/data-grid
v0.5.0
Published
ZUI — an enterprise, server-driven data grid on TanStack Table, styled through ZUI tokens
Downloads
58,297
Maintainers
Readme
@zuilib/data-grid
A server-driven enterprise data grid on TanStack Table,
built from the @zuilib/primitives primitives and styled only through ZUI tokens.
pnpm add @zuilib/data-gridReact and React DOM are the only required peers. TanStack Table, the ZUI primitives, Base UI and tokens install transitively.
For a Tailwind v4 host, load the component and grid source entries once:
@import "tailwindcss";
@import "@zuilib/primitives/tailwind.css";
@import "@zuilib/data-grid/tailwind.css";import DataGrid, { defaultDataGridState, toQuery, toQueryKey } from '@zuilib/data-grid'
const [state, setState] = useState(defaultDataGridState)
const key = toQueryKey(state) // changes only when the request would
useEffect(() => { fetch(toQuery(state)).then(setPage) }, [key])
<DataGrid columns={columns} rows={page} rowCount={total} state={state} onStateChange={setState} loading={loading}>
<DataGrid.Toolbar />
</DataGrid>Leave state out and the grid keeps it itself, starting from defaultState; onStateChange still reports every change. Controlled, every emission is the state prop plus one change: apply it, or ignore it to reject it.
Exports
| Subpath | Exports |
|---------|---------|
| @zuilib/data-grid | Everything below in one entry; DataGrid is the default export |
| @zuilib/data-grid/data-grid | DataGrid (default) with DataGrid.Toolbar, plus the column, label, filter and selection-action types |
| @zuilib/data-grid/use-data-grid | useDataGrid — the TanStack table wiring without the layout, for composing the parts yourself |
| @zuilib/data-grid/server-adapter | toQuery, fromQuery, toQueryKey, toServerState, toColumnState, fromColumnState |
| @zuilib/data-grid/cells | dateCell, dateTimeCell, textColumn, twoValuesColumn |
Component API
DataGrid — @zuilib/data-grid
| Prop | Type | Default |
|------|------|---------|
| columns | DataGridColumnDef<TData>[] | required — TanStack column defs plus the ZUI extras below |
| rows | TData[] | required — the current page of rows, rendered as given |
| rowCount | number | required — total rows on the server (all pages); drives the page count and the range text. Ignored with manualPagination={false}: the rows that pass the filters are counted instead |
| state / defaultState / onStateChange | Partial<DataGridState> | controlled or uncontrolled — onStateChange receives the whole next state on every interaction |
| manualPagination / manualSorting / manualFiltering | boolean | true — the server answers the state; false pages, sorts or filters rows client-side |
| getRowId | (row, index, parent?) => string | row index |
| enableRowSelection | boolean \| (row) => boolean | true — adds the leading checkbox column |
| enableColumnResizing | boolean | true |
| loading | boolean | false — renders skeleton rows (keeping the layout) and marks the table busy |
| skeletonWhileRefreshing | boolean | false — show skeleton rows while loading even when rows are already displayed |
| error | ReactNode | — a string gets the danger Alert; with no rows it replaces the body, with rows it sits above them as a banner |
| emptyState | ReactNode | — replaces the default "No results" empty state |
| density | 'compact' \| 'comfortable' | 'comfortable' |
| stickyHeader | boolean | false — pins the header row to the top of the scroll container (bound it with scrollAreaClassName) |
| onRowClick | (row, event) => void | — |
| onCellCommit | (commit: DataGridCellCommit) => void | — commit of an inline edit (enableInlineEdit on a column) |
| commitOnBlur | boolean | true — an inline edit that loses focus is committed; false drops it instead |
| renderExpanded | (row) => ReactNode | — content of the full-width row under an expanded row; setting it adds the chevron column |
| getRowCanExpand | (row) => boolean | every row when renderExpanded is set |
| selectionActions | DataGridSelectionAction[] | [] — buttons in the bar that appears while rows are selected |
| selectAllMatching | boolean | true — offer "Select all N" once every row on the page is selected and more rows match |
| showPagination | boolean | true — render the pagination footer |
| pageSizeOptions | number[] | [10, 25, 50, 100] |
| labels | Partial<DataGridLabels> | English — every string the grid renders (defaultDataGridLabels lists them) |
| locale | string | user's locale — BCP 47 tag for the numbers the grid formats |
| aria-label | string | 'Data grid' |
| tableOptions | Partial<TableOptions> | — anything else TanStack accepts (defaultColumn, meta, …) |
| children | ReactNode | — <DataGrid.Toolbar> and anything else to render above the table |
| className / tableClassName / scrollAreaClassName | string | root / <table> / scrolling wrapper |
Layout
The root is a CSS container (@container, inline-size containment) and the table scrolls horizontally inside its own wrapper, so the grid fits any column it is placed in and never widens the page; pinned columns and the sticky header work inside that scroller. The parts respond to the grid's width, not the viewport: below 40rem the toolbar search is full width, the column manager button shows only its icon, and the footer is one row (page size, range, previous / next). On touch screens the row controls, head triggers and page buttons reach 44px and the resize handles are visible; desktop keeps the density's geometry. When composing the parts yourself with useDataGrid, put them inside an element with @container for the same behaviour.
DataGridSelectionAction is { id, label, variant?, tone?, onSelect } — variant and tone are the
@zuilib/primitives Button axes (ButtonVariant / ButtonTone); onSelect(selectedIds, { allMatching, rowCount })
receives the selected row ids.
DataGrid.Toolbar
| Prop | Type | Default |
|------|------|---------|
| showSearch | boolean | true — the global search field, debounced into the search state |
| searchPlaceholder | string | labels.searchPlaceholder |
| searchDebounceMs | number | 300 — ms between the last keystroke and the search change |
| showFilterChips | boolean | true — a removable chip per active column filter |
| showColumnManager | boolean | true — visibility, order and pinning in one popover |
| addableColumns | DataGridAddableField[] | — fields offered under "Add column" |
| onAddColumn | (id: string) => void | — the consumer appends the column def |
| children | ReactNode | — rendered between the chips and the column manager |
Columns
DataGridColumnDef is a TanStack ColumnDef plus the ZUI extras — on the def itself or on meta:
| Extra | Type | Effect |
|-------|------|--------|
| align | 'start' \| 'center' \| 'end' | Head and cell alignment; 'end' for numbers |
| width | number | Initial width in px; the user resizes from there |
| filter | { control: 'text' } \| { control: 'select', options } \| { control: 'date-range' } | Declares the column's filter popover |
| enableInlineEdit | boolean | Double-click or Enter turns the cell into an input; see onCellCommit |
| meta.label | string | The column's name in the column manager, filter chips and pin labels when header is not a string |
State
DataGridState holds pagination, sorting, columnFilters, search (the free-text search),
columnVisibility, columnOrder, columnPinning ({ start, end } — logical directions),
rowSelection, expanded, allMatching and optionally columnSizing.
Server adapter — @zuilib/data-grid/server-adapter
Pure functions between DataGridState and your API: toQuery(state) builds the request ({ page, pageSize, sort, filters, search, allMatching }, 1-based page, filters keyed in column-id order) and fromQuery(query) restores the state from one; toQueryKey(state) is a stable string that changes only when the server request would (drop it in a useEffect dependency or a react-query key); toServerState(state) strips the UI-only keys; toColumnState / fromColumnState pack the saved-view keys (visibility, order, pinning) for persistence.
Cells — @zuilib/data-grid/cells
Column helpers for the common enterprise shapes: dateCell / dateTimeCell (locale-formatted presets of createDateCell(options)), textColumn (accessor + header + sorting in one call, on one row field), twoValuesColumn (primary line + muted secondary line, each a { field, render? }).
Headless — @zuilib/data-grid/use-data-grid
useDataGrid(options) is the state and TanStack wiring without the layout: it returns { table, state, setState, replaceState } so the toolbar, selection bar, body and pagination can be composed differently. <DataGrid> is this hook plus the default layout (DataGrid.Toolbar, DataGrid.SelectionBar, DataGrid.Body, DataGrid.Pagination, DataGrid.ColumnManager).
Keyboard
The grid is one tab stop (the row, cell or head last focused). ArrowUp / ArrowDown move between rows, ArrowRight enters the cells and the arrows move between them (ArrowUp from the first row reaches the header); Enter / Space on a cell act on its checkbox or chevron, start an inline edit, or click the row. Shift+ArrowRight / Shift+ArrowLeft open / close an expandable row.
Localisation
labels overrides any string (defaultDataGridLabels lists them); locale formats the numbers.
Bulk selection
Once every row on the page is selected, "Select all N" sets allMatching in the state; toQuery passes it on and selection actions receive {allMatching, rowCount}.
Docs: the "Data grid" section of the ZUI documentation site.
