Data Table
A powerful and flexible data table component for displaying, filtering, sorting, and paginating tabular data.
Reference the TanStack Table Column Definitions Guide for detailed column definition guide.
Features
- Advanced Filtering - Multiple filter types (text, number, date, select, multi-select) with customizable operators
- URL State Management - Sync table state with URL search params using nuqs
- Sorting - Multi-column sorting with persistent state
- Pagination - Server-side or client-side pagination with customizable page sizes
- Column Visibility - Toggle column visibility with view options menu
- Row Selection - Single or multi-row selection with action bar
- Column Pinning - Pin columns to left or right for better UX
- Keyboard Navigation - Full keyboard support with shortcuts for filter and sort menus
- Responsive - Mobile-first design with overflow handling
- Customizable - Flexible API for custom filters, columns, and actions
Layout
Import all components and combine them:
Walkthrough
Define columns with appropriate metadata:
Initialize the table state using the useDataTable hook:
Pass the table instance to the DataTable, and DataTableToolbar components:
Render an action bar on row selection:
useDataTable
A hook for initializing the data table with state management.
Overview
The useDataTable hook acts as an intelligent wrapper around useReactTable from @tanstack/react-table. It seamlessly handles:
- Server-side pagination, sorting, and filtering
- Controlled or uncontrolled state for page and page size
- Debounced column filter updates (defaults to 300ms) to prevent excessive API calls
- Row selection and column visibility management
Import
Usage
Basic Setup
First, define your columns. Use the meta property to configure filter behaviors.
Controlled Pagination (Recommended for API integrations)
When fetching data from an external API, it's highly recommended to control the pagination state externally.
Uncontrolled Pagination
If you don't provide page, perPage, or onPageChange, the hook will manage the state internally.
Column Filtering
Column filtering is enabled by setting enableColumnFilter: true in your column definitions. You can configure column filtering in three different modes:
1. Controlled Column Filtering (Standard SSR / Server-Side Filtering)
In standard server-side filtering (SSR), each column filter maps to its own direct URL query parameter (e.g. ?search=xyz&status=paid,pending).
By passing filterValues and onFilterValuesChange to useDataTable, the table becomes a fully controlled component synced with your URL parameters. When a user types in a text filter or checks a faceted filter:
- The table automatically triggers
onColumnFiltersChange(with built-in debounce for text inputs). - It calls
onFilterValuesChangewith the updated keys and values. - You write the updates back to the URL search params, which triggers a reload/fetch.
Here is a complete step-by-step walkthrough for React Router v7 / Remix:
Step-by-Step Implementation
- Read URL Params: Read the active parameters using
useSearchParams. - Configure
useDataTable: Pass the parsed values tofilterValuesand handle state updates inonFilterValuesChange. - Reset Page on Filter Change: Always reset the
pageparameter to1when filters are modified to prevent displaying empty pages. - Fetch Data: Pass the active URL parameters directly to your backend API fetching hook.
Code Example
2. Client-Side Column Filtering (Without URL parameters)
If you just want local in-memory filtering in the browser without query parameters:
- Fetch all data from the database at once.
- Set
manualFiltering: false. - Do not pass
filterValuesoronFilterValuesChange.
3. API Parameter Helper: resolveFiltersToFlatParams
When integrating with server-side processing, frontend filter states (like TanStack Column Filters) need to be serialized into query parameters matching your backend API specification.
Instead of writing manual mapper functions for every column, the built-in resolveFiltersToFlatParams utility automatically converts an array of ExtendedColumnFilter objects into a flat key-value query object. It supports various backend filter styles (Nested, Django, PostgREST, MongoDB, etc.), range decomposition, and customizable array formatting out of the box.
Import
Configuration Options
The helper accepts a second argument config of type FilterResolutionConfig:
| Property | Type | Description |
|---|---|---|
style | 'flat' | 'suffix' | 'django' | 'nested' | 'prefix' | 'postgrest' | 'custom' | Naming/structure convention for query keys. Default is 'flat'. |
operatorMap | Partial<Record<FilterOperator, string>> | Custom mapping of operators to backend representation (e.g., { inArray: 'in' }). |
arrayFormat | 'comma' | 'repeat' | 'brackets' | Formats multi-select array values (e.g., comma -> a,b, repeat -> [a, b], brackets -> status[]=[a,b]). Default is 'comma'. |
dateFormat | string | Custom date formatting string (dayjs). Default is 'YYYY-MM-DD'. |
flatRangeSuffix | [string, string] | Suffixes for start/end in range queries when style is flat. Default is ['_min', '_max'] for numbers and ['_start', '_end'] for dates. |
formatFilter | Function | Custom hook to manually format any parameter key-value pair. |
joinParamName | string | Parameter key to represent the logical join operator (e.g. _join -> ?_join=or). |
joinOperator | 'and' | 'or' | Active logical join operator. |
columnOptionsMap | Record<string, string[]> | Valid options for computing complement arrays (for ne / notInArray) on flat APIs. |
paramNameMap | Record<string, string> | Rename parameter keys (e.g. { ticket_status: "status" }). |
customMappers | Partial<Record<keyof TData, (value: any, operator: FilterOperator) => Partial<TParams>>> | Per-column custom serializer overrides. |
Guide: Backend Query Styles
Let's assume we have a filter on the column age with a value of 18 and the operator is gte (Greater than or equal). Here is how each style configuration formats the query parameter:
| Style Name | Parameter Structure | Output Example | Typical Backend / Target Framework |
|---|---|---|---|
flat | key=value | ?age=18 | Express, Laravel (Simple query mapping) |
suffix | key_operator=value | ?age_gte=18 | Custom REST APIs, Django (legacy / flat) |
django | key__operator=value | ?age__gte=18 | Django ORM / Django Filter |
nested | key[operator]=value | ?age[gte]=18 | Ruby on Rails, NestJS (TypeORM), Strapi, Koa |
prefix | key[$operator]=value | ?age[$gte]=18 | FeathersJS, MongoDB Query Syntax |
postgrest | key=operator.value | ?age=gte.18 | PostgREST, Supabase REST API |
[!TIP] If
styleis'flat'(default), operators are omitted in the keys (except forisEmpty/isNotEmptywhich generateskey_is_empty=true/false). For other styles, operators are automatically mapped and embedded.
Guide: Range Filters Decomposition (isBetween)
When a developer uses a numeric range (like a slider) or a date range picker, the table filter outputs an operator isBetween with an array value: [minValue, maxValue].
resolveFiltersToFlatParams handles this intelligently based on the chosen style:
- For Flat Style (
style: 'flat'):- Numeric Range: Decomposes into
_minand_maxkeys.{ price: [10, 50] }$\rightarrow$?price_min=10&price_max=50 - Date Range: Decomposes into
_startand_endkeys.{ createdAt: ['2023-01-01', '2023-01-31'] }$\rightarrow$?createdAt_start=2023-01-01&createdAt_end=2023-01-31 - You can customize these suffixes globally using
flatRangeSuffix: ['_min_suffix', '_max_suffix'].
- Numeric Range: Decomposes into
- For Non-Flat Styles (e.g.
nested,suffix,postgrest):- The helper automatically decomposes
isBetweeninto two separate operations:gte(start) andlte(end), then formats them according to the style rules:style: 'nested'$\rightarrow$?price[gte]=10&price[lte]=50style: 'suffix'$\rightarrow$?price_gte=10&price_lte=50style: 'postgrest'$\rightarrow${ price: ['gte.10', 'lte.50'] }(which gets parsed as?price=gte.10&price=lte.50)
- The helper automatically decomposes
Guide: Array Formatting for Multi-Select (arrayFormat)
For columns that support selecting multiple values (e.g., status: ['active', 'inactive']), you can configure how the array values are sent to your API using arrayFormat:
comma(Default): Combines elements into a single comma-separated string.- Output:
?status=active,inactive
- Output:
repeat: Generates a standard JavaScript array value, which standard query-string serializers repeat.- Output:
?status=active&status=inactive
- Output:
brackets: Appends[]to the key name.- Output:
?status[]=active&status[]=inactive
- Output:
Guide: Negation Filtering & columnOptionsMap
When a developer uses a negation filter (such as "Is not" or "Has none of"), the helper resolves it differently depending on your API structure:
- For Flat Style (
style: 'flat'):- Flat APIs typically do not understand operators like
neornotIn. - To support these, you must provide the list of all available options for that column via
columnOptionsMap. - The helper will compute the options complement at frontend-level (e.g. if status options are
['A', 'B', 'C']and you exclude'A', the helper sends?status=B,C).
- Flat APIs typically do not understand operators like
- For Non-Flat Styles (e.g.
nested,suffix,postgrest):- Standard backends support negation natively (e.g.,
status[ne]=pendingorstatus=neq.pending). - Therefore, you do not need
columnOptionsMapfor these styles. You can safely omit it, and the helper will map the negative operator directly into the query key/value.
- Standard backends support negation natively (e.g.,
Usage Examples
1. Rails / NestJS (Nested Brackets style)
2. Django style API
3. PostgREST / Supabase API
4. Fully Custom Hook Override
If your backend requires a very unique format, you can intercept the format logic:
Props
The useDataTable hook accepts the following properties.
Prop
Type
Returns
The useDataTable hook returns an object with the following properties.
Prop
Type
Column Meta for Filter Variant
Utilize the meta property in your column definitions to configure the UI representation of the filter:
Loading State (DataTableSkeleton)
DataTableSkeleton is used to display a loading skeleton that strictly matches the actual layout of the data table. This is extremely useful when combined with React.Suspense or when awaiting data from an API.
Skeleton Usage
Suspense Integration
The recommended pattern when using frameworks like Next.js or React Router v7 with asynchronous data fetching:
Skeleton Props
| Prop | Type | Default | Description |
|---|---|---|---|
columnCount | number | — | Required. The number of columns the skeleton should render. |
rowCount | number | 10 | The number of row skeletons to display. |
filterCount | number | 0 | The number of filter skeleton boxes to display in the top toolbar. |
cellWidths | string[] | ['auto'] | An array of column widths (e.g., ['100px', '200px']). |
withViewOptions | boolean | true | Whether to render the skeleton for the column visibility toggle button. |
withPagination | boolean | true | Whether to render the skeleton for the bottom pagination controls. |
shrinkZero | boolean | false | If true, columns will possess a min-width matching their respective cellWidths. |
className | string | — | Custom CSS class for the main container. |
API Reference
DataTable Props
Props for the main <DataTable /> component:
| Prop | Type | Default | Description |
|---|---|---|---|
table | Table<TData> | — | Required. The table instance returned by useDataTable. |
actionBar | React.ReactNode | — | Optional action bar component to display when rows are selected. |
wrapperClassname | string | — | Custom className for the table's scrollable wrapper container. |
emptyState | React.ReactNode | 'No results.' | Custom content (string or ReactNode) to show when the table has no records. |
stickyHeader | boolean | false | If true, the table header will stick to the top of the container during vertical scroll. |
onEndReached | () => void | — | Callback function triggered when scrolling near the bottom of the table wrapper (infinite scroll). |
isLoadingMore | boolean | false | Used with onEndReached to prevent duplicate load requests when fetching more data. |
loading | boolean | React.ReactNode | false | Display a blur overlay and spinner in the center of the table (initial load). |
isLoading | boolean | false | Deprecated. Use isLoadingMore instead. |
endReachedThreshold | number | 100 | The distance (in pixels) from the bottom of the container to trigger the onEndReached callback. |
DataTablePagination Props
Props for the <DataTablePagination /> component:
| Prop | Type | Default | Description |
|---|---|---|---|
table | Table<TData> | — | Required. The table instance returned by useDataTable. |
pageSizeOptions | number[] | [10, 20, 30, 40, 50] | Options for the page size select dropdown. |
showPageNumbers | boolean | true | Whether to show the "Rows per page" text and the current page info. |
rowsPerPageText | React.ReactNode | 'Rows per page' | Custom text or ReactNode to display next to the page size select dropdown. |
pageText | (page: number, total: number) => React.ReactNode | — | Custom formatting function to render the current page status text (e.g. "Page X of Y"). |
Column Definitions
The column definitions are used to define the columns of the data table.
Properties
Core configuration options for defining columns.
| Prop | Description |
|---|---|
id | Required: Unique identifier for the column |
accessorKey | Required: Key to access the data from the row |
accessorFn | Optional: Custom accessor function to access data |
header | Optional: Custom header component with column props |
cell | Optional: Custom cell component with row props |
meta | Optional: Meta options for accessing column metadata |
enableColumnFilter | By default, the column will not be filtered. Set to `true` to enable filtering |
enableSorting | Enable sorting for this column |
enableHiding | Enable column visibility toggle |
Column Meta
Column meta options for filtering, sorting, and view options.
| Prop | Description |
|---|---|
label | The display name for the column |
placeholder | The placeholder text for filter inputs |
variant | The type of filter to use (`text`, `number`, `select`, etc.) |
options | For select/multi-select filters, an array of options with `label`, `value`, and optional `count` and `icon` |
loading | For select/multi-select filters, whether the options are currently loading from an API (displays a spinner loader) |
range | For range filters, a tuple of `[min, max]` values |
unit | For numeric filters, the unit to display (e.g., 'hr', '$') |
icon | The react component to use as an icon for the column |
presets | For `dateRange` filters, a boolean to enable the default quick selection presets panel or an array of custom `DatePreset` objects |
align | The alignment of the column content and header (`left`, `center`, `right`) |
thClass | Additional CSS class names to apply to the table header cell (`<TableHead>`) |
cellClass | Additional CSS class names to apply to the table body cell (`<TableCell>`) |
Filter Variants
Available filter variants for column meta.
| Title | Description |
|---|---|
text | Text search with contains, equals, etc. |
number | Numeric filters with equals, greater than, less than, etc. |
range | Range filters with minimum and maximum values |
date | Date filters with equals, before, after, etc. |
dateRange | Date range filters with start and end dates |
boolean | Boolean filters with true/false values |
select | Single-select filters with predefined options |
multiSelect | Multi-select filters with predefined options |
How is this guide?