DataTable
High-performance virtualized table for large volumes of tabular data.
Built on @tanstack/react-table + @tanstack/react-virtual, with
sorting, search, resizing, row selection and custom cell rendering
built in.
The data and columns props must be memoized (React.useMemo) —
virtualization recreates internal structures on every new reference and
would become visibly slow without it. This includes inline Cell/Footer
renderers on a column: define them inside the memoized columns array
rather than as fresh JSX on every render, or virtualization pays the same
cost as an unmemoized columns prop.
When to use
| ✅ Use when… | 🚫 Avoid when… |
|---|---|
|
|
Basic usage
Users
import { DataTable, Column } from '@apollion-dsi/core/data-display/data-table';
import { search } from '@apollion-dsi/core/icons';
const data = React.useMemo(() => users.map(({ id, name, age }) => ({ id, name, age })), [users]);
const columns: Column<typeof data[number]>[] = React.useMemo(
() => [
{ Header: 'Name', accessor: 'name' },
{ Header: 'Age', accessor: 'age' },
],
[],
);
<DataTable title="Users" data={data} columns={columns} hasHeader />;Column alignment
By default everything is left-aligned. Use align on each column with
values from the CellAlign enum:
const columns: Column<Row>[] = React.useMemo(
() => [
{ Header: 'Visits', accessor: 'visits', align: CellAlign.right },
{ Header: 'Progress', accessor: 'progress', align: CellAlign.right },
],
[],
);Custom cells
Provide Cell on the column to render any ReactNode (badges,
tooltips, icons, formatters):
Custom Cell
{
Header: 'Progress',
accessor: 'progress',
align: CellAlign.right,
Cell: ({ value }) => `${value}%`,
}Row color by status
Use setRowColor to highlight entire rows based on the data.
Row color by age/visits
<DataTable
data={data}
columns={columns}
setRowColor={(row) => {
if (row.original.age >= 35) return RowColor.alert;
if (row.original.visits <= 5) return RowColor.success;
return undefined;
}}
/>Row selection
Enable the checkbox column with selectableColumn (a string for a title
or true for no title) and listen for changes with onRowSelect.
Multiple selection
<DataTable
data={data}
columns={columns}
selectableColumn="User"
selectableColumnWidth={100}
onRowSelect={setSelected}
/>Footer with totals
Each column can declare Footer for its own total. Enable the
footer strip with hasFooter:
{
Header: 'Age',
accessor: 'age',
align: CellAlign.right,
Footer: ({ table }) => {
const total = table
.getRowModel()
.rows.reduce((sum, r) => sum + r.original.age, 0);
return <>Total: {total}</>;
},
}Custom last row
For a "load more" button or a loading indicator at the bottom of the
virtualized list, pass lastRowComponent — the DataTable renders
it in the last virtual position and injects the absolute positioning via
an internal wrapper, so lastRowComponent never has to deal with style:
<DataTable
data={data}
columns={columns}
lastRowComponent={
<Flex flexDirection="row" wrap="wrap" justifyContent="center" alignItems="center">
<Button text="Load more" variant="outlined" size="small" />
</Flex>
}
/>Client-side pagination
For large datasets where infinite scroll doesn't fit (e.g. listings with
a page header), enable paginated. The DataTable renders a
navigation row between the body and the footer with Anterior/Próximo,
a pageIndex / pageCount indicator and a page size selector.
Client-side pagination
Mostrando 1-2 de 5
1 / 3
<DataTable
data={data}
columns={columns}
paginated
initialPagination={{ pageSize: 25 }}
pageSizeOptions={[10, 25, 50, 100]}
/>Controlled mode:
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
<DataTable data={data} columns={columns} paginated pagination={pagination} onPaginationChange={setPagination} />;Details:
paginated={false}(default) preserves the virtualized scroll behavior.pageSizeOptions={[10]}hides the selector (page size locked).- UI strings (
"Mostrando X-Y de Z","Anterior","Próximo") default to pt-BR and are overridable via thelabelsprop — see Localization. - Sort + filter run BEFORE pagination — the user sees the sorted and filtered results, paginated afterwards.
Expandable rows (tree mode)
For hierarchical data (parent + children with the same shape), pass
getSubRows. The DataTable injects a toggle column (chevron) as the
first column; each row's chevron is indented by
row.depth * 16px to visualize the hierarchy without changing the
alignment of the data columns.
type Node = { id: number; name: string; age: number; children?: Node[] };
const tree = [
{ id: 1, name: 'Ada', age: 36, children: [{ id: 11, name: 'Ada Jr.', age: 12 }] },
{ id: 2, name: 'Linus', age: 55 },
];
<DataTable
data={tree}
columns={columns}
getSubRows={(row) => row.children}
initialExpanded // expand everything on mount
/>;Controlled mode:
const [expanded, setExpanded] = useState({});
<DataTable
data={tree}
columns={columns}
getSubRows={(row) => row.children}
expanded={expanded}
onExpandedChange={setExpanded}
/>;Custom detail panels (rendering an arbitrary panel under a row, without
a parent→child hierarchy) is a separate gap — it conflicts with the
current fixed-height virtualizer and requires opting into measureElement. See
vendors/TanstackReactTable/README.md §"ROADMAP candidates".
Per-column filters (faceted)
Enable per-column filters by passing filterVariant in the column
definition. The DataTable renders an icon in the header that opens a popover
with the appropriate UI, and automatically wires the
getFacetedRowModel + getFacetedUniqueValues + getFacetedMinMaxValues
pipeline from tanstack-react-table.
Three variants out of the box:
| Variant | UI | builtin filterFn | filterValue shape | Faceted helper used |
|---|---|---|---|---|
'select' | Multi-select with checkboxes + counts | arrIncludesSome | string[] (selected values) | getFacetedUniqueValues() |
'range' | 2 min/max inputs | inNumberRange | [min?, max?] (numbers; empty=undefined) | getFacetedMinMaxValues() |
'text' | Contains input (case-insensitive) | includesString | string | — |
The filterValue shape is what lands in columnFilters (controlled mode
below) and what a custom filterFn receives as its third argument. A cleared
filter is undefined, never an empty array/string. The variant union is
exported as FilterVariant.
const columns: Column<User>[] = React.useMemo(
() => [
{ Header: 'Name', accessor: 'name', filterVariant: 'text' },
{ Header: 'Status', accessor: 'status', filterVariant: 'select' },
{ Header: 'Age', accessor: 'age', align: CellAlign.right, filterVariant: 'range' },
// Column that opts out of the per-column filter pipeline:
{ Header: 'Visits', accessor: 'visits', disableFilters: true },
],
[],
);
<DataTable data={data} columns={columns} hasHeader />;Live — open the ⋯ icon on each header (Name = text, Status = select,
Age = range; Visits opts out):
Controlled mode (via columnFilters + onColumnFiltersChange):
const [columnFilters, setColumnFilters] = useState([]);
<DataTable data={data} columns={columns} columnFilters={columnFilters} onColumnFiltersChange={setColumnFilters} />;Overriding filterFn (while keeping the variant's UI):
{
Header: 'Tags',
accessor: 'tags',
filterVariant: 'select',
filterFn: (row, columnId, vals) =>
(row.getValue(columnId) as string[]).some((t) => vals.includes(t)),
}Details:
- The filter icon (
ellipsisH) only appears on columns withfilterVariantdefined ANDdisableFilters !== true. - The icon color reflects
column.getIsFiltered()—primary.actionwhen a filter is active, neutral when empty. - Clicking the filter icon does NOT trigger sort (event propagation blocked).
- Faceted helpers operate per-column (drill-down UX) —
SelectFilteronly shows values still relevant considering other active filters. - Sort + filter + pagination + expansion coexist. Tanstack pipeline:
core → filter (global ∩ column AND) → sort → expand → paginate. - UI strings (
"Filtrar...","Sem valores") default to pt-BR and are overridable via thelabelsprop — see Localization.
Localization
Every runtime UI string of the pagination row and the filter popovers is
overridable through the labels prop (DataTableLabels). The object is
partial — omitted fields keep the pt-BR defaults (a default flip to
English is reserved for v6):
Localized pagination
Showing 1-2 of 5
1 / 3
import { DataTable, DataTableLabels } from '@apollion-dsi/core/data-display/data-table';
const labels: DataTableLabels = {
showingRange: (start, end, total) => `Showing ${start}-${end} of ${total}`,
perPageOption: (size) => `${size} per page`,
perPageTrigger: (size) => `${size} / page`,
previous: 'Previous',
next: 'Next',
filterNoValues: 'No values',
filterPlaceholder: 'Filter...',
filterRangeMin: (min) => `from ${min}`,
filterRangeMax: (max) => `to ${max}`,
};
<DataTable data={data} columns={columns} paginated labels={labels} />;The search placeholder and empty state have their own dedicated props
(searchPlaceholder, emptyTitle, emptyDescription).
Empty state
When data is empty (or all records have been filtered out),
customize the message:
Custom empty state
No content
No results were found.
<DataTable
data={[]}
columns={columns}
emptyIcon={<Icon icon={search} size="large" color="primary.dark" />}
emptyTitle="No content"
emptyDescription="No results were found."
/>Properties
Prop | Type | Default | Description |
|---|---|---|---|
bordered | boolean | — | Adds vertical borders between cells. |
columnFilters | ColumnFiltersState | — | Controlled per-column filters state. Requires `onColumnFiltersChange`. |
columns * | Column<T>[] | — | Column definitions. Must be memoized (`useMemo`). |
data * | T[] | — | Dataset to display. Must be memoized (`useMemo`). |
emptyDescription | string | — | Helper text of the empty state. |
emptyIcon | ReactNode | — | Icon shown when `data` is empty. |
emptyTitle | string | — | Title of the empty state. |
expanded | ExpandedState | — | Controlled expanded state. Requires `onExpandedChange`. |
expandedColumnWidth | number | — | Width of the expand toggle column, in pixels. Defaults to
`selectableColumnWidth` when both coexist; otherwise `48`. |
getSubRows | ((row: T, index: number) => T[]) | — | Returns a row's sub-rows, enabling tree expansion (a chevron toggle
column is injected). Return `undefined` for leaf rows.
@example ```tsx
<DataTable<TreeNode> data={tree} columns={cols} getSubRows={(row) => row.children} />
``` |
hasFooter | boolean | — | Renders the footer row with sums defined in `column.Footer`. |
hasHeader | boolean | — | Shows the header (title + search + side slots). |
hasSpacingMenu | boolean | — | Enables the density menu (compact/default/expansive). |
headerLeftComponent | ReactNode | — | Left slot in the header — after the title. |
headerRightComponent | ReactNode | — | Right slot in the header — after the search input. |
initialColumnFilters | ColumnFiltersState | — | Initial per-column filters state (uncontrolled). Default `[]`.
Each entry has `{ id: <column id>, value: <filter value> }`.
Mutually exclusive with `columnFilters` (controlled). If both
are provided, controlled takes precedence. |
initialExpanded | ExpandedState | — | Initial expanded state (uncontrolled). Default `{}` (all rows
collapsed). Use `true` to expand all initially.
Mutually exclusive with `expanded` (controlled). If both are
provided, controlled takes precedence. |
initialPagination | Partial<PaginationState> | — | Initial pagination state (uncontrolled). Default `{ pageIndex: 0,
pageSize: pageSizeOptions[0] ?? 10 }`.
Mutually exclusive with `pagination` (controlled). If both are
provided, the controlled state takes precedence. |
inputSearchComponent | ReactNode | — | Replaces the global search `Input` with a custom component (useful for
integration with server-side filters). |
labels | DataTableLabels | — | Overrides for the UI strings of the pagination row and the filter
popovers. Partial — omitted fields keep the pt-BR defaults. See
{@link DataTableLabels}. |
lastRowComponent | ReactNode | — | Component pinned to the end of the list (e.g. a "load more" button).
Rendered by `DataTable` at the last virtual position; the
absolute positioning is injected by the internal wrapper (not by
`lastRowComponent` itself). |
onColumnFiltersChange | OnChangeFn<ColumnFiltersState> | — | Callback fired when any per-column filter changes. |
onExpandedChange | OnChangeFn<ExpandedState> | — | Callback fired when the expansion changes. Accepts the tanstack updater pattern. |
onPaginationChange | OnChangeFn<PaginationState> | — | Callback fired when pagination changes. Accepts the tanstack updater pattern. |
onRowClick | ((row: T) => void) | — | Callback fired when any row is clicked. |
onRowSelect | ((d: T[]) => void) | — | Callback fired when the row selection changes. |
pageSizeOptions | number[] | — | Page size options available in the selector. Default `[10, 25, 50, 100]`.
When the array has ≤ 1 entries, the page size selector is hidden
(the consumer locks the page size). |
paginated | boolean | false | Enables client-side pagination. When `true`, DataTable wires
`getPaginationRowModel()` into tanstack-react-table and renders a
navigation row ("Anterior" / "Próximo" + indicator + page size
selector) between the body and the footer.
Default `false` — infinite virtualized scroll behavior
preserved. |
pagination | PaginationState | — | Controlled pagination state. When provided, `onPaginationChange`
must update this value at the callsite. |
searchPlaceholder | string | — | Placeholder of the global search input. |
selectableColumn | string | boolean | — | When provided, shows a checkbox column for row selection.
Accepts a string (column title) or `true` (no title). |
selectableColumnWidth | number | — | Width of the selection column, in pixels. |
setRowColor | ((row: Row<T>) => RowColor) | — | Resolves the background color of each row. Return a {@link RowColor}
value to highlight status (success, alert, error) or `undefined` to use
the default (including zebra striping). |
spacing | number | — | Additional vertical spacing added to the row height. |
striped | boolean | — | Applies a zebra-striped background to even rows. |
title | string | — | Title displayed in the table header (requires `hasHeader`). |
Beyond the props above, every component accepts the layout props (spacing, color, flex/grid, sizing, border) — not repeated here.
See also
- Storybook story: Components / DataTable
- Simple and composite table:
Table