DataTable

Reference for the data table hook, components, column options and filters

Overview

shadcntable is built on TanStack Table v9 and shadcn/ui. The hook owns the table state. Each component takes the table instance, so you can place, restyle or replace them one by one.

ExportFilePurpose
useDataTableuse-data-table.tsCreates the table instance
DataTabledata-table.tsxRenders the header and rows
DataTableToolbardata-table-toolbar.tsxGlobal search, filter reset, view options
DataTablePaginationdata-table-pagination.tsxPage navigation and page size
DataTableColumnHeaderdata-table-column-header.tsxSort and hide menu, filter popover
DataTableViewOptionsdata-table-view-options.tsxColumn visibility menu
createSelectionColumndata-table-selection-column.tsxCheckbox column for row selection
createDataTableColumnHelperlib/features.tsTyped column helper
DataTableLocaleProviderlib/locale.tsxTranslations

Installation

bash
pnpm dlx shadcn@latest add https://shadcntable.com/r/data-table.json

Usage

tsx
'use client'

import { DataTable } from '@/components/shadcntable/data-table'
import { DataTableColumnHeader } from '@/components/shadcntable/data-table-column-header'
import { DataTablePagination } from '@/components/shadcntable/data-table-pagination'
import { DataTableToolbar } from '@/components/shadcntable/data-table-toolbar'
import { createDataTableColumnHelper } from '@/components/shadcntable/lib/features'
import { useDataTable } from '@/components/shadcntable/use-data-table'

type User = { id: string; name: string; email: string }

const columnHelper = createDataTableColumnHelper<User>()

const columns = columnHelper.columns([
  columnHelper.accessor('name', {
    header: ({ column }) => <DataTableColumnHeader column={column} />,
    meta: { label: 'Name' },
  }),
  columnHelper.accessor('email', {
    header: ({ column }) => <DataTableColumnHeader column={column} />,
    meta: { label: 'Email' },
  }),
])

export function UsersTable({ users }: { users: User[] }) {
  const table = useDataTable({ columns, data: users, getRowId: (user) => user.id })

  return (
    <div className='space-y-4'>
      <DataTableToolbar table={table} />
      <DataTable table={table} />
      <DataTablePagination table={table} />
    </div>
  )
}

useDataTable

useDataTable wraps TanStack Table's useTable and registers the features the components need: sorting, column and global filtering, pagination, row selection and column visibility. It accepts every TanStack Table option. The most common ones are below.

PropTypeDefaultDescription
dataTData[]RequiredRows to display. Keep the reference stable between renders.
columnsDataTableColumnDef<TData>[]RequiredColumn definitions, usually built with createDataTableColumnHelper.
initialStatePartial<TableState>-Starting state, e.g. the page size or a default sort.
statePartial<TableState>-Controlled state slices. Pair each one with its on[Slice]Change.
manualPagination / manualSorting / manualFilteringbooleanfalseLeave that step to your server and render data as given.
rowCountnumber-Total rows on the server, used for the page count in manual mode.
enableRowSelectionboolean | (row) => booleantrueWhich rows can be selected.
getRowId(row, index) => string-Stable row ids, so selection survives data changes.

State

Each piece of state (sorting, columnFilters, globalFilter, pagination, rowSelection, columnVisibility) has one owner:

  • The table. This is the default. Read state from table.state and change it with methods such as table.setSorting or column.toggleVisibility.
  • The table, with a starting value. Pass initialState, for example { pagination: { pageIndex: 0, pageSize: 25 } }.
  • You. Pass the value in state and its matching on[Slice]Change callback.
tsx
const [sorting, setSorting] = useState<SortingState>([{ id: 'name', desc: false }])

const table = useDataTable({
  columns,
  data,
  state: { sorting },
  onSortingChange: setSorting,
})

DataTable

PropTypeDefaultDescription
tableDataTableInstance<TData>RequiredThe instance returned by useDataTable.
isLoadingbooleanfalseShows skeleton rows instead of data.
isFetchingbooleanfalseShows a spinner over the current rows.
emptyStateReactNode-Replaces the default "No results." message.
onRowClick(row: DataTableRow<TData>) => void-Makes rows clickable and focusable. Clicks on buttons, links and checkboxes are ignored.
skeletonRowCountnumber5Number of skeleton rows while loading.

DataTableToolbar

PropTypeDefaultDescription
tableDataTableInstance<TData>RequiredThe instance returned by useDataTable.
showSearchbooleantrueShows the global search input.
showViewOptionsbooleantrueShows the column visibility menu.
childrenReactNode-Extra filters or actions, placed after the search input.

The reset button appears while a search or column filter is active. It clears both.

tsx
<DataTableToolbar table={table}>
  <Button size='sm' variant='outline' onClick={exportSelection}>
    Export
  </Button>
</DataTableToolbar>

DataTablePagination

PropTypeDefaultDescription
tableDataTableInstance<TData>RequiredThe instance returned by useDataTable.
pageSizeOptionsnumber[][10, 20, 30, 40, 50]Choices in the rows-per-page menu.
showSelectedCountbooleantrue with a selection columnShows how many rows are selected.

DataTableColumnHeader

PropTypeDefaultDescription
columnDataTableColumn<TData>RequiredThe column from the header context.
titlestringmeta.label ?? column.idText shown in the header.

The header shows only the controls a column supports. Turn them off per column with enableSorting: false or enableHiding: false. A column that can't sort, hide or filter renders as plain text.

Column meta

The components read these fields from each column's meta:

PropTypeDefaultDescription
labelstring-Readable column name for the header and the view options menu.
filterFilterConfig-Adds a filter popover to the column header.

DataTableColumnMeta is an interface. Extend it with declaration merging to add your own fields.

Filters

Set meta.filter on a column to add a filter popover to its header. useDataTable gives the column the matching filter function. A filterFn you set on the column yourself takes precedence.

PropTypeDefaultDescription
variantFilterVariantRequiredtext, select, multiSelect, numberRange, dateRange or custom. Picks the input and the matching logic.
titlestring-Heading at the top of the filter popover.
descriptionstring-Helper text under the title.
placeholderstring-Placeholder for text and select filters.
optionsFilterOption[]-Choices for select and multiSelect: { label, value, icon? }.
debounceMsnumber300Delay before a text filter applies.
componentComponentType<FilterComponentProps>-The input for the custom variant. Receives value and onChange.
VariantInputFilter valueMatches when
textDebounced text inputstringThe value contains the text, ignoring case
selectSingle selectstring | numberThe value equals the option
multiSelectSearchable checklist(string | number)[]The value, or any item of an array value, is selected
numberRangeMin and max inputs[min?, max?]The value is within the range (inclusive)
dateRangeRange calendar{ from?: Date, to?: Date }The date is within the range, whole days inclusive
customYour componentAnythingYour column's filterFn returns true

A missing end of a number or date range leaves that side open.

Custom filters

A custom filter provides the input as component and the matching logic as filterFn:

tsx
columnHelper.accessor('age', {
  header: ({ column }) => <DataTableColumnHeader column={column} />,
  filterFn: (row, columnId, adultsOnly: boolean) =>
    !adultsOnly || row.getValue<number>(columnId) >= 18,
  meta: {
    label: 'Age',
    filter: {
      variant: 'custom',
      component: ({ value, onChange }) => (
        <Checkbox
          checked={value === true}
          onCheckedChange={(checked) => onChange(checked === true || undefined)}
        />
      ),
    },
  },
})

Call onChange(undefined) to remove the filter.

Row selection

Add createSelectionColumn<TData>() as the first column. It adds a checkbox for each row and a "select all" checkbox for the current page in the header.

tsx
const table = useDataTable({
  columns,
  data,
  getRowId: (user) => user.id,
  enableRowSelection: (row) => row.original.status === 'active',
})

const selectedUsers = table.getSelectedRowModel().rows.map((row) => row.original)

Row clicks

tsx
<DataTable table={table} onRowClick={(row) => router.push(`/users/${row.original.id}`)} />

A clickable row can take keyboard focus and responds to Enter and Space. Clicks on buttons, links, checkboxes and menu items inside the row don't count as row clicks.

Loading states

Pass isLoading for the first load and isFetching for background refreshes. isLoading shows skeleton rows, and isFetching shows a spinner over the rows you already have.

tsx
<DataTable table={table} isLoading={query.isPending} isFetching={query.isFetching} />

Localization

Wrap your tables in DataTableLocaleProvider. Any string you don't override keeps its English default. Strings that include numbers are functions.

tsx
import { DataTableLocaleProvider } from '@/components/shadcntable/lib/locale'

;<DataTableLocaleProvider
  locale={{
    toolbar: { searchPlaceholder: 'Rechercher...', reset: 'Réinitialiser' },
    pagination: {
      rowsPerPage: 'Lignes par page',
      pageOf: (page, pageCount) => `Page ${page} sur ${pageCount}`,
      rowsSelected: (selected, total) =>
        `${selected} sur ${total} ligne(s) sélectionnée(s)`,
    },
  }}
>
  <UsersTable users={users} />
</DataTableLocaleProvider>

For every key, see DataTableLocale in lib/locale.tsx.

Server-side data

Set manualPagination, manualSorting or manualFiltering, own that state, and fetch with it. The table then renders your data as given. See the server-side example.