Data table
A table for real data, with sorting, search, row selection, pinned columns, a sticky header and pagination.
| Method | Country | |||||
|---|---|---|---|---|---|---|
| [email protected] | Card | United States | 2026-01-01 | $5.00 | ||
| [email protected] | PayPal | Japan | 2026-02-02 | $84.20 | ||
| [email protected] | Bank | Germany | 2026-03-03 | $163.40 | ||
| [email protected] | Apple Pay | Brazil | 2026-04-04 | $242.60 | ||
| [email protected] | Card | India | 2026-05-05 | $321.80 | ||
| [email protected] | PayPal | United States | 2026-06-06 | $401.00 | ||
| [email protected] | Bank | Japan | 2026-07-07 | $480.10 | ||
| [email protected] | Apple Pay | Germany | 2026-08-08 | $559.30 | ||
| [email protected] | Card | Brazil | 2026-09-09 | $638.50 | ||
| [email protected] | PayPal | India | 2026-01-10 | $717.70 |
pnpm dlx shadcn@latest add https://hextaui.com/r/data-table.jsonAdds the component, the HextaUI theme tokens and any HextaUI components it depends on.
Add the theme tokens to your global CSS, if you haven’t yet.
Install the dependencies.
pnpm add @base-ui/react @tabler/icons-react @tanstack/react-table class-variance-authority cnCopy and paste the following code into your project.
components/ui/data-table.tsx components/ui/table.tsx components/ui/button.tsx components/ui/checkbox.tsx components/ui/skeleton.tsx Update the import paths to match your project setup.
The data table is TanStack Table v9 with the sorting, filtering, pagination, selection and column visibility features already wired up. Define columns once with createDataTableColumns, create the table with useDataTable, then compose the parts you need.
Pass any TanStack state you want to own, such as sorting or row selection, with its change handler.
DataTableColumnHeader, DataTableSelectAll and DataTableSelectRow go in your column definitions, as a column’s header or cell.
Plain table
The plain <Table /> parts the data table is built from. Use them on their own for static data, with a caption and a footer total.
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank transfer | $350.00 |
| INV004 | Paid | Credit card | $450.00 |
| Total | $1,200.00 | ||
Loading
With loading, skeleton rows fill the same space as real rows, so nothing jumps when the data arrives. The table is marked aria-busy meanwhile.
| Method | Country | |||||
|---|---|---|---|---|---|---|
Empty
emptyMessage fills the body when there is no data or nothing matches the search.
| Method | Country | |||||
|---|---|---|---|---|---|---|
| No payments yet. | ||||||
Sticky header
Give the container a max height with containerClassName and set stickyHeader. The header stays put and gains a hairline once the rows scroll under it. Shift-drag over the checkboxes scrolls the box as you near its edge.
| Method | Country | |||||
|---|---|---|---|---|---|---|
| [email protected] | Card | United States | 2026-01-01 | $5.00 | ||
| [email protected] | PayPal | Japan | 2026-02-02 | $84.20 | ||
| [email protected] | Bank | Germany | 2026-03-03 | $163.40 | ||
| [email protected] | Apple Pay | Brazil | 2026-04-04 | $242.60 | ||
| [email protected] | Card | India | 2026-05-05 | $321.80 | ||
| [email protected] | PayPal | United States | 2026-06-06 | $401.00 | ||
| [email protected] | Bank | Japan | 2026-07-07 | $480.10 | ||
| [email protected] | Apple Pay | Germany | 2026-08-08 | $559.30 | ||
| [email protected] | Card | Brazil | 2026-09-09 | $638.50 | ||
| [email protected] | PayPal | India | 2026-01-10 | $717.70 | ||
| [email protected] | Bank | United States | 2026-02-11 | $796.90 | ||
| [email protected] | Apple Pay | Japan | 2026-03-12 | $876.10 | ||
| [email protected] | Card | Germany | 2026-04-13 | $955.30 | ||
| [email protected] | PayPal | Brazil | 2026-05-14 | $34.50 | ||
| [email protected] | Bank | India | 2026-06-15 | $113.70 | ||
| [email protected] | Apple Pay | United States | 2026-07-16 | $192.90 | ||
| [email protected] | Card | Japan | 2026-08-17 | $272.00 | ||
| [email protected] | PayPal | Germany | 2026-09-18 | $351.20 | ||
| [email protected] | Bank | Brazil | 2026-01-19 | $430.40 | ||
| [email protected] | Apple Pay | India | 2026-02-20 | $509.60 | ||
| [email protected] | Card | United States | 2026-03-21 | $588.80 | ||
| [email protected] | PayPal | Japan | 2026-04-22 | $668.00 | ||
| [email protected] | Bank | Germany | 2026-05-23 | $747.20 | ||
| [email protected] | Apple Pay | Brazil | 2026-06-24 | $826.40 | ||
| [email protected] | Card | India | 2026-07-25 | $905.60 | ||
| [email protected] | PayPal | United States | 2026-08-26 | $984.80 | ||
| [email protected] | Bank | Japan | 2026-09-27 | $63.90 | ||
| [email protected] | Apple Pay | Germany | 2026-01-01 | $143.10 | ||
| [email protected] | Card | Brazil | 2026-02-02 | $222.30 | ||
| [email protected] | PayPal | India | 2026-03-03 | $301.50 | ||
| [email protected] | Bank | United States | 2026-04-04 | $380.70 | ||
| [email protected] | Apple Pay | Japan | 2026-05-05 | $459.90 | ||
| [email protected] | Card | Germany | 2026-06-06 | $539.10 | ||
| [email protected] | PayPal | Brazil | 2026-07-07 | $618.30 | ||
| [email protected] | Bank | India | 2026-08-08 | $697.50 | ||
| [email protected] | Apple Pay | United States | 2026-09-09 | $776.70 | ||
| [email protected] | Card | Japan | 2026-01-10 | $855.80 | ||
| [email protected] | PayPal | Germany | 2026-02-11 | $935.00 | ||
| [email protected] | Bank | Brazil | 2026-03-12 | $14.20 | ||
| [email protected] | Apple Pay | India | 2026-04-13 | $93.40 | ||
| [email protected] | Card | United States | 2026-05-14 | $172.60 | ||
| [email protected] | PayPal | Japan | 2026-06-15 | $251.80 | ||
| [email protected] | Bank | Germany | 2026-07-16 | $331.00 | ||
| [email protected] | Apple Pay | Brazil | 2026-08-17 | $410.20 | ||
| [email protected] | Card | India | 2026-09-18 | $489.40 | ||
| [email protected] | PayPal | United States | 2026-01-19 | $568.60 | ||
| [email protected] | Bank | Japan | 2026-02-20 | $647.70 | ||
| [email protected] | Apple Pay | Germany | 2026-03-21 | $726.90 | ||
| [email protected] | Card | Brazil | 2026-04-22 | $806.10 | ||
| [email protected] | PayPal | India | 2026-05-23 | $885.30 | ||
| [email protected] | Bank | United States | 2026-06-24 | $964.50 | ||
| [email protected] | Apple Pay | Japan | 2026-07-25 | $43.70 | ||
| [email protected] | Card | Germany | 2026-08-26 | $122.90 | ||
| [email protected] | PayPal | Brazil | 2026-09-27 | $202.10 | ||
| [email protected] | Bank | India | 2026-01-01 | $281.30 | ||
| [email protected] | Apple Pay | United States | 2026-02-02 | $360.50 | ||
| [email protected] | Card | Japan | 2026-03-03 | $439.60 | ||
| [email protected] | PayPal | Germany | 2026-04-04 | $518.80 | ||
| [email protected] | Bank | Brazil | 2026-05-05 | $598.00 | ||
| [email protected] | Apple Pay | India | 2026-06-06 | $677.20 | ||
| [email protected] | Card | United States | 2026-07-07 | $756.40 | ||
| [email protected] | PayPal | Japan | 2026-08-08 | $835.60 | ||
| [email protected] | Bank | Germany | 2026-09-09 | $914.80 | ||
| [email protected] | Apple Pay | Brazil | 2026-01-10 | $994.00 | ||
| [email protected] | Card | India | 2026-02-11 | $73.20 | ||
| [email protected] | PayPal | United States | 2026-03-12 | $152.40 | ||
| [email protected] | Bank | Japan | 2026-04-13 | $231.50 | ||
| [email protected] | Apple Pay | Germany | 2026-05-14 | $310.70 | ||
| [email protected] | Card | Brazil | 2026-06-15 | $389.90 | ||
| [email protected] | PayPal | India | 2026-07-16 | $469.10 | ||
| [email protected] | Bank | United States | 2026-08-17 | $548.30 | ||
| [email protected] | Apple Pay | Japan | 2026-09-18 | $627.50 | ||
| [email protected] | Card | Germany | 2026-01-19 | $706.70 | ||
| [email protected] | PayPal | Brazil | 2026-02-20 | $785.90 | ||
| [email protected] | Bank | India | 2026-03-21 | $865.10 | ||
| [email protected] | Apple Pay | United States | 2026-04-22 | $944.30 | ||
| [email protected] | Card | Japan | 2026-05-23 | $23.40 | ||
| [email protected] | PayPal | Germany | 2026-06-24 | $102.60 | ||
| [email protected] | Bank | Brazil | 2026-07-25 | $181.80 | ||
| [email protected] | Apple Pay | India | 2026-08-26 | $261.00 | ||
| [email protected] | Card | United States | 2026-09-27 | $340.20 | ||
| [email protected] | PayPal | Japan | 2026-01-01 | $419.40 | ||
| [email protected] | Bank | Germany | 2026-02-02 | $498.60 | ||
| [email protected] | Apple Pay | Brazil | 2026-03-03 | $577.80 | ||
| [email protected] | Card | India | 2026-04-04 | $657.00 | ||
| [email protected] | PayPal | United States | 2026-05-05 | $736.20 | ||
| [email protected] | Bank | Japan | 2026-06-06 | $815.30 | ||
| [email protected] | Apple Pay | Germany | 2026-07-07 | $894.50 | ||
| [email protected] | Card | Brazil | 2026-08-08 | $973.70 | ||
| [email protected] | PayPal | India | 2026-09-09 | $52.90 | ||
| [email protected] | Bank | United States | 2026-01-10 | $132.10 | ||
| [email protected] | Apple Pay | Japan | 2026-02-11 | $211.30 | ||
| [email protected] | Card | Germany | 2026-03-12 | $290.50 | ||
| [email protected] | PayPal | Brazil | 2026-04-13 | $369.70 | ||
| [email protected] | Bank | India | 2026-05-14 | $448.90 | ||
| [email protected] | Apple Pay | United States | 2026-06-15 | $528.10 | ||
| [email protected] | Card | Japan | 2026-07-16 | $607.20 | ||
| [email protected] | PayPal | Germany | 2026-08-17 | $686.40 | ||
| [email protected] | Bank | Brazil | 2026-09-18 | $765.60 | ||
| [email protected] | Apple Pay | India | 2026-01-19 | $844.80 |
Pinned columns
The selection column and the first data column are pinned by default. Choose your own with pinStart. A soft shadow marks the edge once the table scrolls sideways.
| Method | Country | |||||
|---|---|---|---|---|---|---|
| [email protected] | Card | United States | 2026-01-01 | $5.00 | ||
| [email protected] | PayPal | Japan | 2026-02-02 | $84.20 | ||
| [email protected] | Bank | Germany | 2026-03-03 | $163.40 | ||
| [email protected] | Apple Pay | Brazil | 2026-04-04 | $242.60 | ||
| [email protected] | Card | India | 2026-05-05 | $321.80 | ||
| [email protected] | PayPal | United States | 2026-06-06 | $401.00 | ||
| [email protected] | Bank | Japan | 2026-07-07 | $480.10 | ||
| [email protected] | Apple Pay | Germany | 2026-08-08 | $559.30 | ||
| [email protected] | Card | Brazil | 2026-09-09 | $638.50 | ||
| [email protected] | PayPal | India | 2026-01-10 | $717.70 |
Right to left
Every label and count can be replaced with labels and the format functions. Pagination arrows and pinned columns flip with the direction.
| Method | Country | |||||
|---|---|---|---|---|---|---|
| [email protected] | Card | United States | 2026-01-01 | $5.00 | ||
| [email protected] | PayPal | Japan | 2026-02-02 | $84.20 | ||
| [email protected] | Bank | Germany | 2026-03-03 | $163.40 | ||
| [email protected] | Apple Pay | Brazil | 2026-04-04 | $242.60 | ||
| [email protected] | Card | India | 2026-05-05 | $321.80 | ||
| [email protected] | PayPal | United States | 2026-06-06 | $401.00 |
- Click a sortable header to sort ascending, again for descending, and a third time to clear. Shift-click another header to add a secondary sort.
- Shift-click a row checkbox to select every row between it and the last one you clicked. Hold Shift and drag across checkboxes to select or clear a range in one motion.
- Searching matches every column except the selection column and jumps back to the first page.
- Hiding columns from the View menu keeps the menu open, so you can toggle several at once.
| Key | Action |
|---|---|
| Tab | Moves through the search, View menu, sortable headers, row checkboxes and pagination. |
| EnterSpace | Sorts by the focused header. |
| Space | Toggles the focused checkbox. |
| Esc | Clears the search when it has text. |
- Sortable headers carry
aria-sort, and a polite live region announces the new sort and, shortly after typing, the number of results. - The page indicator is a live region, so screen readers hear the new page after pressing next or previous.
- Checkboxes have labels by default. Override them with
aria-labelon<DataTableSelectAll />and<DataTableSelectRow />.
Every part below has to be rendered inside <DataTable />, which shares the table with them.
Takes the TanStack Table options and returns the table. Pages hold 10 rows unless initialState.pagination says otherwise.
| Prop | Type | Default |
|---|---|---|
data | TData[] | – |
columnsBuild them with createDataTableColumns. | ColumnDef[] | – |
getRowIdKeeps selection stable when rows move. Defaults to the row index. | (row: TData) => string | – |
initialState | Partial<TableState> | { pagination: { pageIndex: 0, pageSize: 10 } } |
stateControl sorting, rowSelection, globalFilter, pagination or columnVisibility. | Partial<TableState> | – |
onSortingChangeEach controllable state has a matching handler, like onRowSelectionChange. | OnChangeFn<SortingState> | – |
enableRowSelection | boolean | (row) => boolean | true |
Returns a typed column helper with accessor, display and columns. Set meta: { align: "end" } on numeric columns to align the header and cells.
| Prop | Type | Default |
|---|---|---|
tableThe table returned by useDataTable. | DataTableInstance<TData> | – |
className | string | – |
| Attribute | Description |
|---|---|
data-slot="data-table" | The wrapper around every part. |
data-slot="data-table-announcer" | The visually hidden live region. |
| Prop | Type | Default |
|---|---|---|
emptyMessage | ReactNode | "No results." |
loading | boolean | false |
loadingRowsNumber of skeleton rows while loading. | number | 5 |
pinStartColumn ids to pin to the start edge. | string[] | ["select", firstColumnId] |
stickyHeaderNeeds a max height on the container. | boolean | false |
containerClassNameApplied to the scroll container. | string | – |
swipeSelectShift-drag across checkboxes to select a range. | boolean | true |
classNameApplied to the table element. | string | – |
| Attribute | Description |
|---|---|
data-slot="table-container" | The scroll container. |
data-scrolled-start | Present on the container when it has scrolled away from the start edge. |
data-scrolled-end | Present while there is more to scroll toward the end edge. |
data-scrolled-top | Present once the rows scroll vertically. |
data-swipe-selecting | Present on the container during a shift-drag. |
data-state="selected" | Present on selected rows. |
data-row-id | The row id from getRowId. |
data-slot="data-table-loading-row" | Each skeleton row. |
data-slot="data-table-empty" | The empty row. |
A wrapping row for the search, the View menu and your own filters. Accepts every div prop and carries data-table-toolbar as its data-slot.
| Prop | Type | Default |
|---|---|---|
placeholder | string | "Search…" |
aria-label | string | "Search table" |
clearLabelAccessible name of the clear button. | string | "Clear search" |
| Attribute | Description |
|---|---|
data-slot="data-table-search" | The search field wrapper. |
| Prop | Type | Default |
|---|---|---|
label | string | "View" |
groupLabel | string | "Toggle columns" |
getLabelDefaults to the column’s string header, or its id capitalized. | (column) => string | – |
Lists every column that can hide. Set enableHiding: false on a column to leave it out.
| Attribute | Description |
|---|---|
data-slot="data-table-view-options" | The menu popup. |
| Prop | Type | Default |
|---|---|---|
column | Column | – |
title | string | – |
Renders a sort button for sortable columns and plain text for the rest.
| Attribute | Description |
|---|---|
data-slot="data-table-column-header" | The header wrapper. |
data-sorted | Present on the sort button while the column is sorted. |
aria-sort | On the header cell: ascending, descending or none. |
| Prop | Type | Default |
|---|---|---|
table | Table | – |
aria-label | string | "Select all rows on this page" |
Selects the rows on the current page and shows an indeterminate state when only some are selected.
| Prop | Type | Default |
|---|---|---|
row | Row | – |
aria-label | string | "Select row" |
| Prop | Type | Default |
|---|---|---|
pageSizes | number[] | [10, 20, 50, 100] |
showSelectionShow the selected count instead of the row count. | boolean | true |
labelsrowsPerPage, firstPage, previousPage, nextPage and lastPage. | Partial<DataTablePaginationLabels> | – |
formatSelection | (selected: number, total: number) => ReactNode | "2 of 42 rows selected" |
formatRows | (total: number) => ReactNode | "42 rows" |
formatPage | (page: number, pageCount: number) => ReactNode | "Page 1 of 5" |
| Attribute | Description |
|---|---|
data-slot="data-table-pagination" | The pagination bar. |
Returns the table from the nearest <DataTable />. Use it to build your own toolbar controls, such as a status filter.
| Prop | Type | Default |
|---|---|---|
stickyHeaderPins the header row inside a scrolling container. | boolean | false |
containerClassNameApplied to the scroll container. | string | – |
containerRef | Ref<HTMLDivElement> | – |
| Attribute | Description |
|---|---|
data-slot="table" | The table element. |
data-sticky-header | Present on the container when stickyHeader is on. |
--table-bg | Row and pinned-cell background. Follows the card or popover it sits in. |
| Prop | Type | Default |
|---|---|---|
alignEnd-aligned cells also use tabular numbers. | "start" | "center" | "end" | "start" |
pinnedKeeps the cell in place while the table scrolls sideways. Offset with --pin-offset. | "start" | "end" | – |
pinnedEdgeDraws a soft shadow on the last pinned column while scrolled. | boolean | false |
| Attribute | Description |
|---|---|
data-align | The current alignment. |
data-pinned | start or end when pinned. |
data-pinned-edge | Present on the last pinned cell of a side. |
--pin-offset | Distance from the pinned edge, set for you. |
TableHeader, TableBody, TableFooter, TableRow and TableCaption render the matching table elements and accept all of their props.