Skip to content

Data · Data table

Data table

A real table, not a spreadsheet. Semantic markup keeps screen readers and browser find-in-page working, while columns describe intent — numeric, currency, percent, status, sparkline — instead of repeating alignment and formatting rules on every screen.

Examples

import { ColumnVisibilityMenu, DataTable, DataTablePagination, DataTableSelectionCount, DataTableToolbar, FacetFilter, SegmentedControl, SegmentedItem, } from "@luc/ui";

Full table

Sorting, global search, faceted filters, selection, column visibility, sticky header, pinned edges, expansion and pagination — in one table, all keyboard reachable.

Forty-eight prepared research runs with performance and metadata columns.
Expand rowEquity
ETH-USDTrend breakoutDraft2.07-0.3%Sep 21
BTC-USDVolatility carryPaused2.0041.7%Sep 13
BTC-USDFunding basisDraft1.9719.5%Sep 23
GCTrend breakoutDraft1.8730.9%Sep 22
AAPLCross-sectional momentumLive1.73-3.8%Aug 24
GCFunding basisPaused1.7335.0%Sep 20
GCRegime switchPaused1.601.9%Sep 8
NDXVolatility carryLive1.5523.0%Sep 9
NVDATrend breakoutLive1.5421.7%Sep 19
NVDARegime switchPaused1.4732.1%Sep 4
NVDARegime switchDraft1.4519.9%Aug 17
ETH-USDVolatility carryDraft1.3637.9%Sep 16

1–12 of 48 runs

Rows per page
1 / 4

The state switch is a demo control: the component just renders the state you give it.

Columns carry the semantics

A column helper decides alignment, tabular figures, em-dash handling and tone. Nothing guesses units: a value formatted as a percentage is one because the column said so.

What the helpers do

  • Numeric columns are right-aligned with tabular figures.
  • null, NaN and Infinity render an em dash — never “NaN”.
  • tone: "signed" uses the semantic positive/negative tokens.
  • Filtering is opt-in per column, and the predicate is named.
  • Locked columns cannot be hidden; pinned columns stay opaque.

What they never do

  • Compute return, volatility, drawdown or any other metric.
  • Convert units (0.0001 is never silently 0.01%).
  • Fetch data — the table renders the rows you give it.
  • Own sort/filter/page state when you control it yourself.
  • Turn into a spreadsheet: no cell editing or fill handles.
const columns = [ textColumn("symbol", { header: "Symbol", locked: true, pin: "start" }), numericColumn("sharpe", { header: "Sharpe", decimals: 2, tone: "signed" }), percentColumn("cagr", { header: "CAGR", decimals: 1 }), statusColumn("status", { header: "Status", filterable: true, filterFn: "arrHas", map: toStatus }), ]; <DataTable columns={columns} data={runs} getRowId={(row) => row.id} ariaLabel="Research runs" />

Server-driven, without a rewrite

Pass manualPagination, manualSorting and the matching controlled state and the table stops slicing locally. Row identity, selection and column visibility keep working because they were never tied to the client row model.

The toolbar is a slot. LUC ships the bound pieces — search, selection count, column menu, facets, pagination — and stays out of the rest.

API

Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.

Exported symbols
ExportKindSignature
DataTable57 documented props<TData extends RowData>({ ariaLabel, ariaDescription, caption, state: tableState, loadingRows, emptyState, errorState, density, stickyHeader, maxHeight, showColumnBorders, toolbar, footer, renderSubR…

DataTable props

Type: DataTableProps<TData>

Values the compiler reads from the declaration.

DataTable props
PropTypeDefaultNotes
ariaDescription?string | undefined—Longer description announced with the table.
ariaLabelstring—Accessible name for the table. Required — an unnamed table is a defect.
caption?ReactNode—
className?string | undefined—
columnFilters?ColumnFiltersState | undefined—
columnPinning?ColumnPinningState | undefined—
columnsreadonly DataTableColumn<TData>[]—
columnVisibility?ColumnVisibilityState | undefined—
dataTData[]—
defaultColumnFilters?ColumnFiltersState | undefined—
defaultColumnPinning?ColumnPinningState | undefined—
defaultColumnVisibility?ColumnVisibilityState | undefined—
defaultExpanded?ExpandedState | undefined—
defaultGlobalFilter?string | undefined—
defaultPagination?PaginationState | undefined—
defaultRowSelection?RowSelectionState | undefined—
defaultSorting?SortingState | undefined—
density?DataTableDensity | undefined—
emptyState?ReactNode—
enableColumnVisibility?boolean | undefined—
enableExpansion?boolean | undefined—
enableFiltering?boolean | undefined—
enableGlobalFilter?boolean | undefined—
enableRowSelection?boolean | "single" | undefined—Adds a leading checkbox column. `"single"` restricts to one row.
enableSorting?boolean | undefined—
errorState?ReactNode—
expanded?ExpandedState | undefined—
footer?ReactNode—Rendered inside the table context, below the scroll area.
getRowId?((row: TData, index: number) => string) | undefined—Stable row identity. Required for selection to survive sorting/filtering.
globalFilter?string | undefined—
loadingRows?number | undefined—
manualFiltering?boolean | undefined—
manualPagination?boolean | undefined—Server-side modes: the table stops slicing and expects prepared data.
manualSorting?boolean | undefined—
maxHeight?string | number | undefined—Constrains the scroll area, which is what makes a sticky header useful.
onColumnFiltersChange?((filters: ColumnFiltersState) => void) | undefined—
onColumnPinningChange?((pinning: ColumnPinningState) => void) | undefined—
onColumnVisibilityChange?((visibility: ColumnVisibilityState) => void) | undefined—
onExpandedChange?((expanded: ExpandedState) => void) | undefined—
onGlobalFilterChange?((value: string) => void) | undefined—
onPaginationChange?((pagination: PaginationState) => void) | undefined—
onRowClick?((row: TData) => void) | undefined—
onRowSelectionChange?((selection: RowSelectionState) => void) | undefined—
onSortingChange?((sorting: SortingState) => void) | undefined—
pageCount?number | undefined—Total pages / rows known to the server.
pageSize?number | undefined—Default page size when the caller does not control pagination.
pagination?PaginationState | undefined—
renderSubRow?((row: TData) => ReactNode) | undefined—Expanded row content. Keep it short; this is a detail strip, not a page.
rowClassName?((row: TData) => string | undefined) | undefined—
rowCount?number | undefined—
rowSelection?RowSelectionState | undefined—
scrollClassName?string | undefined—
showColumnBorders?boolean | undefined—Vertical rules between cells. Off by default — hairlines beat a grid.
sorting?SortingState | undefined—
state?DataTableState | undefined—`loading` and `error` replace the body; `empty` covers zero filtered rows.
stickyHeader?boolean | undefined—
toolbar?ReactNode—Rendered inside the table context, above the scroll area.

Usage

import { DataTable } from "@luc/ui/data-table";
<DataTable
  columns={columns}
  data={runs}
  ariaLabel="Research runs"
  enableSorting
  enableFiltering
/>

Guidelines

Do

  • Always pass `ariaLabel` or `caption`; that is what names the table for assistive technology.
  • Use the built-in `enableSorting`, `enableFiltering` and `enableRowSelection` instead of rebuilding them beside the table, so the header, toolbar and pager read one state.

Don't

  • Render a table for key/value pairs: two columns and six rows are a definition list, and `DataDetails` already gives it a surface.
  • Format numbers inside a cell by hand: return the formatted string from the column definition so the table does not own a locale decision.

Accessibility

It renders a real table with a caption, `th scope="col"` headers and `aria-sort` on the sorted column. Selection and expansion controls carry names such as `Select row 3` and `Collapse row`.

Luc UI v1.0DataTabledark theme