Docs
DataTable

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…
  • Lists with hundreds or thousands of records that need to scroll without jank (payroll, statements, inventory).
  • Scenarios that require per-column sorting, global search and header resizing without extra code.
  • When you want multi-select with checkboxes and a change callback.
  • Small, static tables with few records. Use Table — it has a composite API (<TableHeader>, <TableBody>, <TableRow>), is lighter and allows semantic HTML structure (<table>/<tr>/<td>).
  • When each row's content is heterogeneous (forms, content blocks). Prefer List or custom layouts.

Basic usage

Users
Name
Age
Visits
Progress (%)
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
Name
Progress
Visits
{
  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
Name
Age
Visits
Progress (%)
<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
Name
Age
Visits
Progress (%)
<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
Name
Age
Visits
Progress (%)

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 the labels prop — 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:

VariantUIbuiltin filterFnfilterValue shapeFaceted helper used
'select'Multi-select with checkboxes + countsarrIncludesSomestring[] (selected values)getFacetedUniqueValues()
'range'2 min/max inputsinNumberRange[min?, max?] (numbers; empty=undefined)getFacetedMinMaxValues()
'text'Contains input (case-insensitive)includesStringstring

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):

Name
Status
Age
Visits

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 with filterVariant defined AND disableFilters !== true.
  • The icon color reflects column.getIsFiltered()primary.action when 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) — SelectFilter only 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 the labels prop — 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
Name
Age
Visits
Progress (%)

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
Name
Age
Visits
Progress (%)
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