Zephora UI

Data

DataTable

Generic, fully typed data table with single or multiple sorting, typed per-column and global filtering, client or server-side pagination with a numbered pager, single/multiple row selection, column visibility, grouped (colspan) header rows, summary/footer rows, CSV export, per-column menus with pinning, windowed rendering, loading skeletons/overlay and an empty state.

Import

import { DataTable, dataTableToCsv } from "@zephora/react";

Examples

Sorting and pagination

`DataTable` is generic over the row type — pass it explicitly for full type-safety in `columns`, `rowKey` and `cell` renderers. Click a header to cycle asc → desc → off.

Ada LovelaceEngineer36
Alan TuringMathematician41
Barbara LiskovProfessor84
Donald KnuthProfessor87

Row selection and global filter

Selection is fully controlled; the header checkbox selects every row that matches the current filters.

Ada LovelaceEngineer36
Grace HopperAdmiral85
Alan TuringMathematician41
Katherine JohnsonPhysicist101
Margaret HamiltonEngineer88
Edsger DijkstraProfessor72
Barbara LiskovProfessor84
Donald KnuthProfessor87
0 row(s) selected

Loading and empty states

No people match your filters.

Server-side (manual) mode

`manual` renders `data` exactly as given — no client-side sorting, filtering or page slicing — and only reports state changes through the callbacks. Pair it with `pagination.page` + `pagination.total` (and here a simulated server that sorts and slices).

Ada LovelaceEngineer36
Grace HopperAdmiral85
Alan TuringMathematician41

Expandable rows

`expandable.render` adds a leading toggle column and a full-width detail row; `onRowClick` makes whole rows interactive (click, plus Enter/Space when focused).

Expand row
Ada LovelaceEngineer36

Ada LovelaceEngineer, age 36.

Grace HopperAdmiral85
Alan TuringMathematician41
Katherine JohnsonPhysicist101
Last clicked: none

Typed filters

`filterType` picks the filter editor and operator set per column: `number` gets =, ≠, >, ≥, <, ≤ and between, `select` a multi-select fed by `filterOptions` (derived from unique column values, max 50, when omitted), `date` on/before/after/between. Filter values are a plain string (classic contains) or `{ op, value }`.

NameRoleAge
Grace HopperAdmiral85
Alan TuringMathematician41
Margaret HamiltonEngineer88
Edsger DijkstraProfessor72
Barbara LiskovProfessor84
Donald KnuthProfessor87

Selection modes

`selection.mode="single"` renders radios and drops the select-all header. Rows failing `isRowSelectable` render a disabled input and are excluded from select-all. In multiple mode, Shift+Click selects the range since the last toggled row. `showColumnMenu` adds a ⋮ menu to each data column header with sort, hide and pin actions.

Select row
Ada LovelaceEngineer36
Grace HopperAdmiral85
Alan TuringMathematician41
Katherine JohnsonPhysicist101
Margaret HamiltonEngineer88
Selected: none

Header groups & summary

`headerGroups` adds a colspan header row above the main header — each group spans its visible member columns; reordering splits non-contiguous members into runs sharing the same label. `column.footer` renders a `<tfoot>` summary row; the function form receives the PROCESSED rows (filtered and sorted, before the pagination slice), so aggregates cover every matching row. `stickyFooter` keeps it pinned to the bottom edge while scrolling.

PersonStats
Ada LovelaceEngineer36
Grace HopperAdmiral85
Alan TuringMathematician41
Katherine JohnsonPhysicist101
Margaret HamiltonEngineer88
Edsger DijkstraProfessor72
Barbara LiskovProfessor84
Donald KnuthProfessor87
Average age74.3

Exports & the imperative ref API

Attach a `ref` to get a `DataTableHandle` with programmatic methods — everything stays optional, nothing changes without the ref. `exportCsv` mirrors the built-in button; `exportXlsx` writes a real dependency-free `.xlsx` workbook (styled header, frozen row, auto-filter, numeric cells); `exportPdf` opens a print-optimized document to save as PDF (full Unicode). All three export the CURRENT view: filtered + sorted rows, visible columns. `getProcessedRows`/`getSelectedRows` expose the same data for custom pipelines.

Ada LovelaceEngineer36
Grace HopperAdmiral85
Alan TuringMathematician41
Katherine JohnsonPhysicist101
Margaret HamiltonEngineer88

API

DataTable<T> props

PropTypeDefaultDescription
columns *Array<DataTableColumn<T>>Column definitions (see the table below).
data *T[]Row objects.
rowKey *(row: T) => stringStable unique key per row.
sort / defaultSort / onSortChangeDataTableSort | nullControlled / uncontrolled sort state: `{ key, direction: "asc" | "desc" }`.
sortMode"single" | "multiple""single""single" keeps the classic one-column cycle; "multiple" builds an ordered sort list — clicking a column appends it asc, then cycles desc, then removes it.
multiSort / defaultMultiSort / onMultiSortChangeDataTableSort[]Controlled / uncontrolled multi-sort state (used when sortMode="multiple").
filters / defaultFilters / onFiltersChangeRecord<string, DataTableFilterValue>Controlled / uncontrolled per-column filter values. A plain string keeps the classic case-insensitive contains semantics (back-compat); the object form `{ op, value? }` selects an explicit operator matching the column's `filterType` (value: `string | number | [number, number] | string[]`).
DataTableFilterOpstring unionOperators available per filter type — text: "contains" | "equals" | "startsWith" | "endsWith" | "notContains" | "isEmpty"; number: "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "between"; select: "in"; date: "on" | "before" | "after" | "between".
globalFilterstringMatches rows where any column contains the query.
paginationDataTablePaginationEnables the pager: { pageSize, page? (controlled, 0-based), total? (server-side row count), onPageChange?, pageSizeOptions? (renders a page-size select labeled "Rows per page"), onPageSizeChange?, allowAll? (appends a show-everything option), allLabel? (its label, default "All") }. The pager renders numbered page buttons with boundary/sibling ellipses plus a "from–to of total" range readout. Reset policy: while the page is uncontrolled, any filter, global-filter or sort change resets it to 0 (firing onPageChange), and picking a page size also resets to 0; a controlled page is never written to — only onPageChange fires.
stateKeystringPersists the user's view state — column order, visibility, pinning, resized widths, page size and sort — to storage under this key and restores it on mount. Uncontrolled aspects only; controlled props stay the source of truth. Clear via the ref handle's clearPersistedState(). SSR note: restoration happens on the client, so persisted tables should be client-only or accept default-state first paint.
stateStorage"local" | "session""local"Storage backing stateKey.
selectionDataTableSelection<T>{ mode? ("multiple" default; "single" renders radios and no select-all header), selected? / defaultSelected? / onSelectionChange? (controlled or uncontrolled row keys), isRowSelectable? (rows returning false render a disabled input and are excluded from select-all) }. In multiple mode, Shift+Click selects the visible-row range between the last toggled row and the target, respecting isRowSelectable.
virtualize{ height: number; rowHeight: number; overscan?: number }Simple windowing: renders only the visible rows plus spacers. `overscan` (default 4) renders extra rows above/below the visible window. Not supported together with `expandable` — when both are provided the table warns in the console and ignores `expandable`.
manualbooleanfalseServer-side mode — shorthand that turns on all three manual* flags: data renders as-is (no client-side sorting, filtering or page slicing) and state changes are only reported through the sort/filter/page callbacks. Combine with pagination.total + pagination.page.
manualSorting / manualFiltering / manualPaginationbooleanmanualGranular server-side flags — each disables only its own client-side step (sorting, filtering or page slicing). Each defaults to `manual`, so any one can be overridden individually.
onRowClick(row: T, event: MouseEvent | KeyboardEvent) => voidMakes rows interactive: click, plus Enter/Space when the row is focused.
expandableDataTableExpandable<T>Row expansion: { render(row), expandedKeys?, defaultExpandedKeys?, onExpandedChange?, rowExpandable? } — adds a leading toggle column and a full-width detail row. Rows where rowExpandable returns false render an empty toggle cell (no button) and never show a detail row. Ignored (with a console warning) while `virtualize` is set.
resizableColumnsbooleanfalseAdds pointer + keyboard resize handles to header edges.
reorderableColumnsbooleanfalseEnables header drag-and-drop column reordering (HTML5 draggable). Pass columnOrder for programmatic/keyboard-free control.
columnOrder / defaultColumnOrder / onColumnOrderChangestring[]Controlled / uncontrolled column order (list of column keys).
columnVisibility / defaultColumnVisibility / onColumnVisibilityChangeRecord<string, boolean>Controlled / uncontrolled column visibility. `false` hides a column; missing keys are visible. Hidden columns are excluded from render/pin/order math but stay in state (and their filters keep applying).
showColumnVisibilityMenubooleanfalseRenders a toolbar button opening a checkbox menu to toggle column visibility.
headerGroupsDataTableHeaderGroup[]Extra header row above the main header: each `{ header, columns }` group spans its visible member columns with `<th colSpan scope="colgroup">`. Groups render in the order their first member appears; members made non-contiguous by reordering are split into contiguous runs sharing the same label. Ungrouped columns (and the select/expand helpers) get empty spanning cells. Compatible with `stickyHeader` — the group row sticks above the main header and filter rows.
stickyFooterbooleanfalseKeeps the summary/footer row (`column.footer`) stuck to the bottom edge while scrolling.
showExportButtonbooleanfalseRenders a toolbar button that downloads the PROCESSED rows (filtered and sorted, ignoring pagination) as CSV — visible columns only, in current order. Serialization follows the `dataTableToCsv` rules below.
exportOptionsDataTableExportOptionsOptions for the export button: { filename? (default "table.csv"), delimiter? (default ","), includeHeaders? (default true), bom? (default true) }.
showColumnMenubooleanfalseAdds a per-column ⋮ menu button to each data column header with sort, hide and pin actions. Items appear per column capability: sort entries only for sortable columns, unpin only while the column is (effectively) pinned.
columnPinning / defaultColumnPinning / onColumnPinningChangeRecord<string, "start" | "end" | false>Controlled / uncontrolled per-column pin state overriding `column.pin`; `false` explicitly unpins. Effective pin = `columnPinning[key] ?? column.pin`.
loadingbooleanfalseLoading state (sets aria-busy). With no rows, skeleton rows render (as many as the page size, 3 without pagination). With rows present, the current rows stay visible, dimmed under a spinner overlay (role="status").
emptyReactNode"No data"Content shown when there are no rows.
getRowClassName(row: T, index: number) => string | undefinedClass applied to each body `<tr>`.
getRowStyle(row: T, index: number) => CSSProperties | undefinedInline style applied to each body `<tr>`.
size"sm" | "md" | "lg""md"Density scale.
stripedbooleanfalseAlternating row backgrounds.
stickyHeaderbooleanfalseKeeps the header row pinned while scrolling.
unstyledbooleanfalseHeadless mode — skips Zephora styling.

DataTableColumn<T> props

PropTypeDefaultDescription
key *stringUnique column id; also the default row property read for values.
header *ReactNodeHeader cell content.
cell(row: T, index: number) => ReactNodeCustom cell renderer.
accessor(row: T) => unknownValue accessor used for sorting/filtering. Defaults to `row[key]`.
sortablebooleanRenders the header as a sort button (asc → desc → off).
filterablebooleanAdds a per-column filter input in a second header row.
widthnumber | stringFixed column inline-size.
align"start" | "center" | "end"Cell text alignment.
pin"start" | "end"Pins the column to the start or end edge (sticky positioning).
sortFn(a: T, b: T) => numberCustom comparator (receives whole rows). Overrides the built-in type-aware comparison in both single and multiple sort modes; return a negative/zero/positive number like `Array.prototype.sort`.
filterType"text" | "number" | "select" | "date""text"Filter editor rendered in the filter row and the value semantics used when filtering.
filterOptionsArray<{ label: string; value: string }>Options for filterType="select"; derived from unique column values (max 50) when omitted.
headerTextstringPlain-text header fallback used for accessible labels and the column visibility menu when `header` is not a string.
headerTooltipstringNative tooltip (title attribute) shown when hovering the header cell.
footerReactNode | ((rows: T[]) => ReactNode)Summary/footer cell content. The function form receives the CURRENTLY PROCESSED rows — filtered and sorted, but BEFORE the pagination slice — so aggregates cover every matching row, not just the visible page. A `<tfoot>` row renders when any visible column defines `footer`.

dataTableToCsv(columns, rows, options?) props

PropTypeDefaultDescription
columns *Array<DataTableColumn<T>>Columns to serialize. Cell text comes from `column.accessor` (else `row[key]`); non-string headers fall back to `headerText ?? key`.
rows *T[]Rows to serialize. The helper is a pure standalone export — usable without the component.
optionsDataTableCsvOptions{ delimiter? (default ","), includeHeaders? (default true), bom? (default true — prefixes the UTF-8 BOM so Excel detects the encoding) }.
returnsstringRFC 4180 CSV: fields containing quotes, the delimiter or newlines are quoted with doubled quotes; lines are joined with CRLF.

DataTableHandle<T> (ref) props

PropTypeDefaultDescription
exportCsv(options?: DataTableExportOptions) => voidDownloads the current view (filtered + sorted rows, visible columns) as CSV. Merges over the table's `exportOptions`.
exportXlsx(options?: DataTableXlsxExportOptions) => Promise<void>Builds and downloads a real .xlsx workbook with zero dependencies (stored-ZIP + SpreadsheetML). Options: filename, sheetName, headerFill/headerColor/cellColor (#rrggbb), orientation, columnWidths (chars, per key), freezeHeader (default true), autoFilter (default true). Numbers export as numeric cells; the writer loads lazily on first call.
exportPdf(options?: DataTablePdfOptions) => Promise<void>Renders a print-optimized document (repeating header, zebra rows, page orientation) into a hidden iframe and opens the print dialog to save as PDF. Full Unicode — no jsPDF/font subsetting. Options: title, orientation, fontSize, fontFamily, headerFill/headerColor/cellColor, striped, columnWidths (CSS values).
getProcessedRows() => T[]Rows after filtering and sorting, before the pagination slice — the exact set exports operate on.
getSelectedRows() => T[]Currently selected rows (empty when selection is disabled).
clearPersistedState() => voidRemoves the view state saved under stateKey (no-op without one).
elementHTMLDivElement | nullThe table's root element for measurements or scrolling.

Keyboard

KeyAction
TabMoves through sort buttons, filter inputs, checkboxes and the pager.
Enter / SpaceActivates the focused sort button (cycles asc → desc → off).
SpaceToggles the focused row / select-all checkbox.
Shift + ClickOn a row checkbox (multiple selection mode): selects the visible-row range between the last toggled row and the clicked row, respecting isRowSelectable.