Data Table
A data table on TanStack Table: sort on several columns, filter from the column definitions, select rows with a floating action bar, expand, pin, page and export to CSV — with controlled state for server-side data. The free Data Grid stays the small one.
Usage
Installation
Data Table comes with WunderUI Core and Pro, in the package from your plan. See the plans.
Examples
Keyboard
The table is a grid with one tab stop. Arrow keys move between cells, Home and End jump along the row, Ctrl+Home and Ctrl+End to the first and last cell, Page Up and Page Down ten rows. Enter or Space does a cell's main thing (sort a column, tick a checkbox), Shift+Enter adds a column to the sort, F2 moves into the controls of a cell and Escape comes back. Tab leaves the table.
Multi-sort
Click a header to sort by it (ascending, descending, off). Shift-click, or Shift+Enter, or “Then by” in the column menu adds a column to the sort; a number shows the order. aria-sort is set on every sortable header.
Filters from the column definitions
A column with meta.filter is filterable: the Filter button opens a command palette of those columns, each filter becomes a chip you can edit or remove. Types: text, select, multiSelect and number (a range). Select options are read from the rows unless you list them.
Selection and the action bar
Row checkboxes select, Shift-click selects a range, the header checkbox selects the page. While rows are selected a floating bar shows the count, your bulkActions, Select all, Export selected and Clear.
Expandable rows
Pass renderSubRow and each row gets a chevron that opens a panel under it. getRowCanExpand turns it off for single rows (refunded orders have no details here).
Column pinning
Pin a column to the start or the end from its menu, or set columnPinning. Pinned columns stay put while the rest scrolls sideways. The checkbox and expander columns always stay first.
Server-side data
serverSide hands paging, sorting and filtering to your API. onQueryChange fires with { pagination, sorting, columnFilters, globalFilter } whenever one changes; you answer with data (one page, already in order) and rowCount. Here the “API” waits 650 ms; make the next request fail to see the error state and its retry.
Controlled state
Every part of the state is controlled once you pass its value and its onChange: sorting, columnFilters, globalFilter, columnVisibility, rowSelection, pagination, columnPinning and expanded. Parts you leave out keep working on their own, seeded by initialState.
State in the URL
useDataTableUrlState keeps page, sort, filters and search in the address bar with history.replaceState: the table starts from the URL it finds, so a view can be linked and reloaded. Values equal to the defaults are left out, other parameters stay, and prefix separates several tables on one page. parseDataTableQuery and serializeDataTableQuery are the two halves if you want to do the routing yourself (the preview here only shows the query string it would write).
CSV export
Export writes the visible columns of every row after filtering and sorting; with a selection, the action bar exports just those. Cells that start with = + - or @ get a leading apostrophe so spreadsheets do not run them. onExport replaces the download, for exports your server makes.
Loading
loading shows skeleton rows in the real column widths and sets aria-busy on the grid. Toolbar and pager stay in place.
Empty
Without data the table says so; give emptyState your own message and call to action. When a search or filter caused the empty result, the table offers Reset filters instead.
Error
error replaces the rows with a message (true, a string or { title, description }); onRetry adds the Try again button. The message is announced as an alert.
In context
How the component is used inside real screens — the same compositions as the Usage section of the Figma page.
Invoices in a card
In a settings or billing page the table sits inside a Card; className drops its own frame.