Qotra UI

Data Table

A fully-featured data table built on TanStack Table with advanced filtering, sorting, pagination, column pinning, row selection, and mobile card view.

A heavily extended data table built on TanStack Table and shadcn/ui. State (filters, sorting, pagination) is synced to the URL via nuqs, making every table view shareable and bookmarkable.

Installation

Install the component via the shadcn CLI:

npx shadcn add @qotra/data-table

This adds the following files to your project:

  • components/data-table/ — all table components
  • hooks/use-data-table.ts — the core table state hook
  • lib/data-table.ts — utility functions
  • lib/parsers.ts — nuqs parsers for URL state
  • config/data-table.ts — filter/sort operator config
  • types/data-table.ts — shared TypeScript types

Wrap your app with NuqsAdapter

URL state management requires the nuqs adapter at your app root:

app/root.tsx
import { NuqsAdapter } from 'nuqs/adapters/react-router/v7';

export default function Root() {
  return (
    <NuqsAdapter>
      <App />
    </NuqsAdapter>
  );
}

Dependencies

npm packages: @tanstack/react-table, nuqs, nanoid, zod, react-day-picker, lucide-react

shadcn components: table, button, input, select, badge, skeleton, calendar, popover, command, dropdown-menu, separator, slider, label


Usage

1. Define columns

Each column needs an id, an accessorKey (or accessorFn), and a meta block describing how it should be filtered and displayed.

import type { ColumnDef } from '@tanstack/react-table';
import { Text, CalendarIcon, Tag } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header';

interface Task {
  id: string;
  title: string;
  status: 'todo' | 'in-progress' | 'done';
  priority: 'low' | 'medium' | 'high';
  createdAt: Date;
}

const columns: ColumnDef<Task>[] = [
  {
    id: 'title',
    accessorKey: 'title',
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Title" />
    ),
    cell: ({ row }) => <span>{row.getValue('title')}</span>,
    meta: {
      label: 'Title',
      placeholder: 'Search titles...',
      variant: 'text',
      icon: Text,
    },
    enableColumnFilter: true,
  },
  {
    id: 'status',
    accessorKey: 'status',
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    meta: {
      label: 'Status',
      variant: 'multiSelect',
      icon: Tag,
      options: [
        { label: 'Todo', value: 'todo' },
        { label: 'In Progress', value: 'in-progress' },
        { label: 'Done', value: 'done' },
      ],
    },
    enableColumnFilter: true,
  },
  {
    id: 'createdAt',
    accessorKey: 'createdAt',
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Created" />
    ),
    meta: {
      label: 'Created At',
      variant: 'dateRange',
      icon: CalendarIcon,
    },
    enableColumnFilter: true,
  },
];

2. Initialize the table

Use the useDataTable hook to manage all table state — pagination, sorting, and filtering are automatically synced to the URL.

import { useDataTable } from '@/hooks/use-data-table';

function TasksTable({ data, pageCount }: { data: Task[]; pageCount: number }) {
  const { table } = useDataTable({
    data,
    columns,
    pageCount,
    initialState: {
      sorting: [{ id: 'createdAt', desc: true }],
      pagination: { pageSize: 20 },
    },
    getRowId: (row) => row.id,
  });

  return (
    <DataTable table={table}>
      <DataTableToolbar table={table}>
        <DataTableSortList table={table} />
      </DataTableToolbar>
    </DataTable>
  );
}

3. Render the table

import { DataTable } from '@/components/data-table/data-table';
import { DataTableToolbar } from '@/components/data-table/data-table-toolbar';
import { DataTableSortList } from '@/components/data-table/data-table-sort-list';

<DataTable table={table}>
  <DataTableToolbar table={table}>
    <DataTableSortList table={table} />
  </DataTableToolbar>
</DataTable>

Advanced toolbar with filter list

Swap in DataTableAdvancedToolbar and DataTableFilterList for a persistent filter row:

import { DataTableAdvancedToolbar } from '@/components/data-table/data-table-advanced-toolbar';
import { DataTableFilterList } from '@/components/data-table/data-table-filter-list';
import { DataTableSortList } from '@/components/data-table/data-table-sort-list';

<DataTable table={table}>
  <DataTableAdvancedToolbar table={table}>
    <DataTableFilterList table={table} />
    <DataTableSortList table={table} />
  </DataTableAdvancedToolbar>
</DataTable>

Make sure to pass enableAdvancedFilter: true to useDataTable when using advanced filtering:

const { table } = useDataTable({
  data,
  columns,
  pageCount,
  enableAdvancedFilter: true, 
});

Command-palette filter menu

Replace DataTableFilterList with DataTableFilterMenu for a keyboard-driven command palette interface:

import { DataTableFilterMenu } from '@/components/data-table/data-table-filter-menu';

<DataTable table={table}>
  <DataTableAdvancedToolbar table={table}>
    <DataTableFilterMenu table={table} />
    <DataTableSortList table={table} />
  </DataTableAdvancedToolbar>
</DataTable>

Bulk action bar

Show a floating action bar when rows are selected:

import { BulkActionBar } from '@/components/data-table/bulk-action-bar';
import { Button } from '@/components/ui/button';

function TaskActionBar({ table }: { table: Table<Task> }) {
  const rows = table.getFilteredSelectedRowModel().rows;

  return (
    <BulkActionBar
      count={rows.length}
      noun="task"
      onClear={() => table.toggleAllRowsSelected(false)}
    >
      <Button
        variant="outline"
        size="sm"
        onClick={() => handleDelete(rows.map((r) => r.original.id))}
      >
        Delete
      </Button>
    </BulkActionBar>
  );
}

<DataTable table={table} actionBar={<TaskActionBar table={table} />}>
  <DataTableToolbar table={table} />
</DataTable>

Loading state

Pass isLoading to show skeleton rows while data is fetching:

<DataTable table={table} isLoading={isFetching}>
  <DataTableToolbar table={table} />
</DataTable>

Mobile infinite scroll

On mobile, the table automatically switches to a card layout. Pass hasMore and onLoadMore to enable infinite scroll:

<DataTable
  table={table}
  hasMore={hasNextPage}
  onLoadMore={fetchNextPage}
>
  <DataTableToolbar table={table} />
</DataTable>

Server-side data fetching

The hook exposes parsed URL state values you can pass directly to your server or API:

const { table, page, perPage, sorting, filterValues } = useDataTable({
  data,
  columns,
  pageCount,
  enableAdvancedFilter: true,
});

// Use these in your data fetching (e.g. with React Router loaders):
// page        → current page number
// perPage     → page size
// sorting     → [{ id: 'createdAt', desc: true }]
// filterValues → advanced filter state from the URL

With React Router, read the URL params directly in your loader:

export async function loader({ request }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const page = Number(url.searchParams.get('page') ?? 1);
  const perPage = Number(url.searchParams.get('perPage') ?? 20);
  const sort = url.searchParams.get('sort');

  const { data, total } = await getTasks({ page, perPage, sort });

  return { data, pageCount: Math.ceil(total / perPage) };
}

API Reference

useDataTable

The core hook for initializing table state. All pagination, sorting, and filter state is synced to the URL automatically.

PropTypeDefaultDescription
dataTData[]The row data to display
columnsColumnDef<TData>[]Column definitions
pageCountnumberTotal number of pages (required for server-side pagination)
initialStatePartial<TableState>Initial sorting, pagination, and visibility state
getRowId(row: TData) => stringFunction to derive a unique ID per row, used for stable row selection
queryKeysPartial<QueryKeys>Override default URL param names (page, perPage, sort, filters, joinOperator)
history'push' | 'replace''replace'Whether URL changes push a new history entry or replace the current one
debounceMsnumber300Debounce delay for filter input changes
throttleMsnumber50Throttle delay for URL state writes
clearOnDefaultbooleanfalseRemove URL params when values match their defaults
enableAdvancedFilterbooleanfalseEnable advanced filter mode (required for DataTableFilterList / DataTableFilterMenu)
scrollbooleanfalseScroll to top on pagination change
shallowbooleantrueUse shallow routing for URL updates
startTransitionReact.TransitionStartFunctionWrap URL state updates in a React transition

Returns: { table, page, perPage, sorting, filterValues, shallow, debounceMs, throttleMs }


DataTable

The main table container. Renders a standard table on desktop and switches to a card list on mobile.

PropTypeDefaultDescription
tableTable<TData>The table instance from useDataTable
actionBarReact.ReactNodeContent to render in the bulk action bar slot
isLoadingbooleanfalseShow skeleton rows instead of data
hasMorebooleanWhether more rows can be loaded (mobile infinite scroll)
onLoadMore() => voidCalled when the user scrolls to the bottom on mobile

DataTableToolbar

Standard toolbar with a search input and column visibility toggle. Place sort/filter controls as children.

PropTypeDescription
tableTable<TData>The table instance
childrenReact.ReactNodeAdditional toolbar controls (e.g. DataTableSortList)

DataTableAdvancedToolbar

Extended toolbar designed to host DataTableFilterList or DataTableFilterMenu alongside sort controls.

PropTypeDescription
tableTable<TData>The table instance
childrenReact.ReactNodeFilter and sort components

DataTableFilterList

Renders active filters as removable chips with inline editors. Supports adding new filters from a dropdown.

PropTypeDescription
tableTable<TData>The table instance

DataTableFilterMenu

A command palette-style interface for adding and editing filters. Opened with Cmd/Ctrl + Shift + F.

PropTypeDescription
tableTable<TData>The table instance

DataTableSortList

Renders active sorts as removable chips, with a dropdown to add new sort rules. Supports drag-to-reorder. Opened with Cmd/Ctrl + Shift + S.

PropTypeDescription
tableTable<TData>The table instance

DataTableColumnHeader

Drop-in column header with sort toggle and hide controls.

PropTypeDescription
columnColumn<TData>The TanStack column instance
titlestringDisplay label

DataTablePagination

Page size selector and page navigation controls. Included inside DataTable by default — only use this directly if you need a custom layout.

PropTypeDescription
tableTable<TData>The table instance

DataTableViewOptions

Dropdown to toggle column visibility. Included in DataTableToolbar — use directly for custom toolbar layouts.

PropTypeDescription
tableTable<TData>The table instance

BulkActionBar

A fixed floating bar that appears when rows are selected. Renders via a React portal so it sits above all other content.

PropTypeDefaultDescription
countnumberNumber of selected rows. Bar is hidden when 0
nounstringSingular noun for the item type (e.g. 'task') — pluralized automatically
onClear() => voidCalled when the user clicks the clear button
loadingbooleanfalseShow a loading spinner instead of the count
childrenReact.ReactNodeAction buttons

Column meta

The meta field on each column definition controls filtering and display behavior.

FieldTypeDescription
labelstringDisplay name used in filter UI and sort list
placeholderstringPlaceholder text for text filter inputs
variantFilterVariantWhich filter type to use (see below)
optionsOption[]Options for select and multiSelect filters
range[number, number]Min/max bounds for range and slider filters
unitstringUnit suffix for numeric filters (e.g. 'hr', '$')
iconReact.FCIcon component shown alongside the column name

Filter variants

VariantDescription
textText search — contains, equals, is empty, etc.
numberNumeric — equals, greater than, less than, between, etc.
rangeDual-input numeric range with min and max
dateDate picker — before, after, on, between, relative to today
dateRangeDate range picker with start and end
booleanTrue / false toggle
selectSingle option from a predefined list
multiSelectOne or more options from a predefined list

Types

Option

interface Option {
  label: string;
  value: string;
  count?: number;
  icon?: React.FC<React.SVGProps<SVGSVGElement>>;
}

QueryKeys

Override the default URL parameter names used for table state.

interface QueryKeys {
  page: string;       // default: 'page'
  perPage: string;    // default: 'perPage'
  sort: string;       // default: 'sort'
  filters: string;    // default: 'filters'
  joinOperator: string; // default: 'joinOperator'
}

DataTableRowAction

A helper type for typed row action handlers.

interface DataTableRowAction<TData> {
  row: Row<TData>;
  variant: 'update' | 'delete';
}

Keyboard shortcuts

ShortcutAction
Cmd/Ctrl + Shift + FToggle filter menu
Cmd/Ctrl + Shift + SToggle sort menu
Backspace / DeleteRemove focused filter or sort item; removes the last item when the menu trigger is focused

On this page