# Shadcn Data Table

> Free shadcn/ui data table for React — a reusable <DataTable> built on TanStack Table over the Table primitive. Sorting, row selection, faceted filters, column visibility, row actions, and pagination. The examples official shadcn never shipped.

Source: https://designrevision.com/components/data-table

The **Shadcn Data Table** is the [Table](/components/table) with a brain — a reusable `<DataTable>` built on <a href="https://tanstack.com/table" rel="nofollow">**TanStack Table**</a> that adds sorting, filtering, faceted filters, column visibility, row selection, row actions, and pagination. shadcn ships this as a step-by-step *guide*; we package it as a drop-in component plus the standard helpers, with examples that build up from a basic table to the full "tasks" showcase.

## Built on TanStack Table

The Data Table is the static [Table](/components/table) primitive driven by <a href="https://tanstack.com/table" rel="nofollow">**`@tanstack/react-table`**</a> — a **headless** library that owns the data logic (the row models for sorting, filtering, pagination, faceting, selection, expansion) and leaves the markup to you. shadcn supplies the markup via the Table primitive; we wire the two together. Install it once:

```bash
npm install @tanstack/react-table
```

## Table vs Data Table

Reach for the right one:

- **[Table](/components/table)** — static, styled markup with no dependency. It already does sorting, selection, and pagination in **plain React state** for moderate data.
- **Data Table** — a **column-driven** table where one engine handles sorting, filtering, faceted filters, visibility, and pagination together. Worth it once the feature set grows.

## The `<DataTable>` wrapper

Rather than hand-wiring `useReactTable` every time, pass your `columns` and `data` to the wrapper. It owns the state (sorting, filters, visibility, selection), the render loop, the empty state, and the pagination bar:

```tsx
<DataTable columns={columns} data={data} />
```

Columns are TanStack `ColumnDef`s — `accessorKey`, a `header`, and an optional `cell` renderer. For anything above the table (search, filters, view options) pass a **`toolbar`** render-prop, which receives the live `table` instance:

```tsx
<DataTable columns={columns} data={data} toolbar={(table) => <MyToolbar table={table} />} />
```

## Sorting

Render a column's header with `DataTableColumnHeader` and it becomes a menu with **Asc / Desc / Hide**, backed by `getSortedRowModel()`:

```tsx
header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />
```

The menu uses [Dropdown Menu](/components/dropdown-menu), so it's fully keyboard-navigable. The Data Table example has three sortable columns.

## Filtering & faceted filters

A simple filter is an [Input](/components/input) bound to a column (the Filter toolbar example). A **faceted filter** is the data-table classic — a multi-select dropdown ([Popover](/components/popover) + [Command](/components/command)) with live counts and selected-value badges:

```tsx
<DataTableFacetedFilter column={table.getColumn("status")} title="Status" options={statuses} />
```

Give the column a `filterFn` that checks array membership and the facet counts populate from `getFacetedUniqueValues()`. The Faceted filters example filters tasks by status.

## Selection, visibility & row actions

- **Row selection** — add a checkbox column whose header drives `toggleAllPageRowsSelected` (with an indeterminate state) and whose cell drives `row.toggleSelected`. The pagination bar reports "N of M selected".
- **Column visibility** — drop `<DataTableViewOptions table={table} />` in the toolbar for a [Dropdown Menu](/components/dropdown-menu) of show/hide checkboxes.
- **Row actions** — add an actions column rendering `<DataTableRowActions row={row} />` — a "⋯" menu with edit, copy, and a destructive delete.

The **Full tasks table** example assembles all of these — selection, sortable headers, two faceted filters, global search, view options, row actions, and pagination — the layout everyone means by "shadcn data table".

## Server-side data

The wrapper is **client-side**, which is ideal for hundreds to a few thousand rows. For large or server-paged datasets, TanStack supports **manual mode**: set `manualSorting` / `manualFiltering` / `manualPagination`, drive the state from your query, and fetch each page from your API instead of the client row models.

## Accessibility & theming

The Data Table inherits the [Table](/components/table)'s semantic HTML, and the menu-driven pieces use [Dropdown Menu](/components/dropdown-menu)'s roving-focus keyboard semantics, so headers, actions, and view options are all keyboard-operable. Everything reads your design tokens — `--border`, `--muted`, `--accent`, `--primary` — so it follows your theme, including dark mode, with no overrides.

## Installation

### data-table

**Install:**

```bash
npx shadcn@latest add @designrevision/data-table
```

**Dependencies:** @tanstack/react-table, utils, table, button, @designrevision/select, badge, dropdown-menu, popover, command

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| columns * | ColumnDef<TData, TValue>[] | — | TanStack column definitions (accessorKey, header, cell). |
| data * | TData[] | — | The row data. |
| toolbar | (table) => React.ReactNode | — | Optional toolbar rendered above the table; receives the table instance for search, faceted filters, and view options. |

`*` required.

**Usage & accessibility:** A reusable Data Table built on @tanstack/react-table over the Table primitive. <DataTable columns data /> wires up sorting, filtering, pagination, row selection, column visibility, and faceting; pass a toolbar render-prop for search/facets/view-options. Ships with DataTableColumnHeader, DataTablePagination, DataTableViewOptions, DataTableFacetedFilter, and DataTableRowActions helpers. The menu-driven pieces (column header, view options, row actions) use Dropdown Menu; faceted filters use Popover + Command.

```tsx
// components/ui/data-table.tsx
"use client"

export { DataTableColumnHeader } from "./data-table-column-header"
export { DataTablePagination } from "./data-table-pagination"
export { DataTableViewOptions } from "./data-table-view-options"
export { DataTableFacetedFilter } from "./data-table-faceted-filter"
export { DataTableRowActions } from "./data-table-row-actions"

import * as React from "react"
import {
  type ColumnDef,
  type ColumnFiltersState,
  type SortingState,
  type Table as TanStackTable,
  type VisibilityState,
  flexRender,
  getCoreRowModel,
  getFacetedRowModel,
  getFacetedUniqueValues,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from "@tanstack/react-table"

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
import { DataTablePagination } from "./data-table-pagination"

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[]
  data: TData[]
  /** Optional toolbar rendered above the table; receives the table instance. */
  toolbar?: (table: TanStackTable<TData>) => React.ReactNode
}

export function DataTable<TData, TValue>({
  columns,
  data,
  toolbar,
}: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = React.useState<SortingState>([])
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
    []
  )
  const [columnVisibility, setColumnVisibility] =
    React.useState<VisibilityState>({})
  const [rowSelection, setRowSelection] = React.useState({})

  const table = useReactTable({
    data,
    columns,
    state: { sorting, columnFilters, columnVisibility, rowSelection },
    enableRowSelection: true,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFacetedRowModel: getFacetedRowModel(),
    getFacetedUniqueValues: getFacetedUniqueValues(),
  })

  return (
    <div className="space-y-4">
      {toolbar?.(table)}
      <div className="rounded-md border">
        <Table>
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => (
                  <TableHead key={header.id} colSpan={header.colSpan}>
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                  </TableHead>
                ))}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {table.getRowModel().rows?.length ? (
              table.getRowModel().rows.map((row) => (
                <TableRow
                  key={row.id}
                  data-state={row.getIsSelected() && "selected"}
                >
                  {row.getVisibleCells().map((cell) => (
                    <TableCell key={cell.id}>
                      {flexRender(
                        cell.column.columnDef.cell,
                        cell.getContext()
                      )}
                    </TableCell>
                  ))}
                </TableRow>
              ))
            ) : (
              <TableRow>
                <TableCell
                  colSpan={columns.length}
                  className="h-24 text-center"
                >
                  No results.
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>
      </div>
      <DataTablePagination table={table} />
    </div>
  )
}
```

```tsx
// components/ui/data-table-pagination.tsx
"use client"

import {
  ChevronLeftIcon,
  ChevronRightIcon,
  ChevronsLeftIcon,
  ChevronsRightIcon,
} from "lucide-react"
import { type Table } from "@tanstack/react-table"

import { Button } from "@/components/ui/button"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

interface DataTablePaginationProps<TData> {
  table: Table<TData>
  pageSizeOptions?: number[]
}

export function DataTablePagination<TData>({
  table,
  pageSizeOptions = [5, 10, 20, 30],
}: DataTablePaginationProps<TData>) {
  return (
    <div className="flex flex-wrap items-center justify-between gap-4 px-2">
      <div className="text-muted-foreground flex-1 text-sm whitespace-nowrap">
        {table.getFilteredSelectedRowModel().rows.length} of{" "}
        {table.getFilteredRowModel().rows.length} row(s) selected.
      </div>
      <div className="flex items-center gap-4 lg:gap-8">
        <div className="flex items-center gap-2">
          <p className="text-sm font-medium">Rows per page</p>
          <Select
            value={`${table.getState().pagination.pageSize}`}
            onValueChange={(value) => table.setPageSize(Number(value))}
          >
            <SelectTrigger className="h-8 w-[70px]">
              <SelectValue placeholder={table.getState().pagination.pageSize} />
            </SelectTrigger>
            <SelectContent side="top">
              {pageSizeOptions.map((size) => (
                <SelectItem key={size} value={`${size}`}>
                  {size}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
        <div className="flex w-[100px] items-center justify-center text-sm font-medium">
          Page {table.getState().pagination.pageIndex + 1} of{" "}
          {table.getPageCount()}
        </div>
        <div className="flex items-center gap-2">
          <Button
            variant="outline"
            size="icon"
            className="hidden size-8 lg:flex"
            onClick={() => table.setPageIndex(0)}
            disabled={!table.getCanPreviousPage()}
            aria-label="Go to first page"
          >
            <ChevronsLeftIcon />
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="size-8"
            onClick={() => table.previousPage()}
            disabled={!table.getCanPreviousPage()}
            aria-label="Go to previous page"
          >
            <ChevronLeftIcon />
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="size-8"
            onClick={() => table.nextPage()}
            disabled={!table.getCanNextPage()}
            aria-label="Go to next page"
          >
            <ChevronRightIcon />
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="hidden size-8 lg:flex"
            onClick={() => table.setPageIndex(table.getPageCount() - 1)}
            disabled={!table.getCanNextPage()}
            aria-label="Go to last page"
          >
            <ChevronsRightIcon />
          </Button>
        </div>
      </div>
    </div>
  )
}
```

```tsx
// components/ui/data-table-column-header.tsx
"use client"

import {
  ArrowDownIcon,
  ArrowUpIcon,
  ChevronsUpDownIcon,
  EyeOffIcon,
} from "lucide-react"
import { type Column } from "@tanstack/react-table"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

interface DataTableColumnHeaderProps<TData, TValue>
  extends React.HTMLAttributes<HTMLDivElement> {
  column: Column<TData, TValue>
  title: string
}

export function DataTableColumnHeader<TData, TValue>({
  column,
  title,
  className,
}: DataTableColumnHeaderProps<TData, TValue>) {
  if (!column.getCanSort()) {
    return <div className={cn(className)}>{title}</div>
  }

  return (
    <div className={cn("flex items-center gap-2", className)}>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button
            variant="ghost"
            size="sm"
            className="data-[state=open]:bg-accent -ml-3 h-8"
          >
            <span>{title}</span>
            {column.getIsSorted() === "desc" ? (
              <ArrowDownIcon />
            ) : column.getIsSorted() === "asc" ? (
              <ArrowUpIcon />
            ) : (
              <ChevronsUpDownIcon />
            )}
          </Button>
        </DropdownMenuTrigger>
        <DropdownMenuContent align="start">
          <DropdownMenuItem onClick={() => column.toggleSorting(false)}>
            <ArrowUpIcon className="text-muted-foreground/70" />
            Asc
          </DropdownMenuItem>
          <DropdownMenuItem onClick={() => column.toggleSorting(true)}>
            <ArrowDownIcon className="text-muted-foreground/70" />
            Desc
          </DropdownMenuItem>
          {column.getCanHide() && (
            <>
              <DropdownMenuSeparator />
              <DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
                <EyeOffIcon className="text-muted-foreground/70" />
                Hide
              </DropdownMenuItem>
            </>
          )}
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  )
}
```

```tsx
// components/ui/data-table-view-options.tsx
"use client"

import { Settings2Icon } from "lucide-react"
import { type Table } from "@tanstack/react-table"

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

interface DataTableViewOptionsProps<TData> {
  table: Table<TData>
}

export function DataTableViewOptions<TData>({
  table,
}: DataTableViewOptionsProps<TData>) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="sm" className="ml-auto h-8">
          <Settings2Icon />
          View
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end" className="w-[150px]">
        <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
        <DropdownMenuSeparator />
        {table
          .getAllColumns()
          .filter(
            (column) =>
              typeof column.accessorFn !== "undefined" && column.getCanHide()
          )
          .map((column) => (
            <DropdownMenuCheckboxItem
              key={column.id}
              className="capitalize"
              checked={column.getIsVisible()}
              onCheckedChange={(value) => column.toggleVisibility(!!value)}
            >
              {column.id}
            </DropdownMenuCheckboxItem>
          ))}
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

```tsx
// components/ui/data-table-faceted-filter.tsx
"use client"

import * as React from "react"
import { CheckIcon, PlusCircleIcon } from "lucide-react"
import { type Column } from "@tanstack/react-table"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

interface FacetedFilterOption {
  label: string
  value: string
  icon?: React.ComponentType<{ className?: string }>
}

interface DataTableFacetedFilterProps<TData, TValue> {
  column?: Column<TData, TValue>
  title?: string
  options: FacetedFilterOption[]
}

export function DataTableFacetedFilter<TData, TValue>({
  column,
  title,
  options,
}: DataTableFacetedFilterProps<TData, TValue>) {
  const facets = column?.getFacetedUniqueValues()
  const selectedValues = new Set(column?.getFilterValue() as string[])

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline" size="sm" className="h-8 border-dashed">
          <PlusCircleIcon />
          {title}
          {selectedValues?.size > 0 && (
            <>
              <div className="bg-border mx-2 h-4 w-px" />
              <Badge
                variant="secondary"
                className="rounded-sm px-1 font-normal lg:hidden"
              >
                {selectedValues.size}
              </Badge>
              <div className="hidden gap-1 lg:flex">
                {selectedValues.size > 2 ? (
                  <Badge
                    variant="secondary"
                    className="rounded-sm px-1 font-normal"
                  >
                    {selectedValues.size} selected
                  </Badge>
                ) : (
                  options
                    .filter((option) => selectedValues.has(option.value))
                    .map((option) => (
                      <Badge
                        variant="secondary"
                        key={option.value}
                        className="rounded-sm px-1 font-normal"
                      >
                        {option.label}
                      </Badge>
                    ))
                )}
              </div>
            </>
          )}
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-[200px] p-0" align="start">
        <Command>
          <CommandInput placeholder={title} />
          <CommandList>
            <CommandEmpty>No results found.</CommandEmpty>
            <CommandGroup>
              {options.map((option) => {
                const isSelected = selectedValues.has(option.value)
                return (
                  <CommandItem
                    key={option.value}
                    onSelect={() => {
                      if (isSelected) {
                        selectedValues.delete(option.value)
                      } else {
                        selectedValues.add(option.value)
                      }
                      const filterValues = Array.from(selectedValues)
                      column?.setFilterValue(
                        filterValues.length ? filterValues : undefined
                      )
                    }}
                  >
                    <div
                      className={cn(
                        "flex size-4 items-center justify-center rounded-[4px] border",
                        isSelected
                          ? "bg-primary border-primary text-primary-foreground"
                          : "border-input [&_svg]:invisible"
                      )}
                    >
                      <CheckIcon className="size-3.5" />
                    </div>
                    {option.icon && (
                      <option.icon className="text-muted-foreground size-4" />
                    )}
                    <span>{option.label}</span>
                    {facets?.get(option.value) && (
                      <span className="ml-auto flex size-4 items-center justify-center font-mono text-xs">
                        {facets.get(option.value)}
                      </span>
                    )}
                  </CommandItem>
                )
              })}
            </CommandGroup>
            {selectedValues.size > 0 && (
              <>
                <CommandSeparator />
                <CommandGroup>
                  <CommandItem
                    onSelect={() => column?.setFilterValue(undefined)}
                    className="justify-center text-center"
                  >
                    Clear filters
                  </CommandItem>
                </CommandGroup>
              </>
            )}
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  )
}
```

```tsx
// components/ui/data-table-row-actions.tsx
"use client"

import { MoreHorizontalIcon } from "lucide-react"
import { type Row } from "@tanstack/react-table"

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

interface DataTableRowActionsProps<TData> {
  row: Row<TData>
  onEdit?: (row: Row<TData>) => void
  onDelete?: (row: Row<TData>) => void
}

export function DataTableRowActions<TData>({
  row,
  onEdit,
  onDelete,
}: DataTableRowActionsProps<TData>) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button
          variant="ghost"
          size="icon"
          className="data-[state=open]:bg-muted size-8"
        >
          <MoreHorizontalIcon />
          <span className="sr-only">Open menu</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end" className="w-[160px]">
        <DropdownMenuItem onClick={() => onEdit?.(row)}>Edit</DropdownMenuItem>
        <DropdownMenuItem>Make a copy</DropdownMenuItem>
        <DropdownMenuItem>Favorite</DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive" onClick={() => onDelete?.(row)}>
          Delete
          <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

## Examples

### Data Table

The reusable `<DataTable columns data />` over TanStack Table — sortable column-header menus, pagination, and an empty state out of the box.

**Install:**

```bash
npx shadcn@latest add @designrevision/data-table-demo-01
```

**Dependencies:** @designrevision/data-table

```tsx
// components/ui/data-table-demo-01.tsx
"use client"

import { type ColumnDef } from "@tanstack/react-table"

import { DataTable, DataTableColumnHeader } from "@/components/ui/data-table"

type Payment = {
  id: string
  amount: number
  status: "pending" | "processing" | "success" | "failed"
  email: string
}

const data: Payment[] = [
  { id: "1", amount: 316, status: "success", email: "ken99@example.com" },
  { id: "2", amount: 242, status: "success", email: "abe45@example.com" },
  { id: "3", amount: 837, status: "processing", email: "monserrat44@example.com" },
  { id: "4", amount: 874, status: "success", email: "silas22@example.com" },
  { id: "5", amount: 721, status: "failed", email: "carmella@example.com" },
  { id: "6", amount: 459, status: "pending", email: "jordan78@example.com" },
  { id: "7", amount: 633, status: "processing", email: "nina@example.com" },
  { id: "8", amount: 192, status: "success", email: "leo@example.com" },
  { id: "9", amount: 548, status: "failed", email: "priya@example.com" },
  { id: "10", amount: 387, status: "pending", email: "marco@example.com" },
  { id: "11", amount: 905, status: "success", email: "dana@example.com" },
  { id: "12", amount: 264, status: "processing", email: "omar@example.com" },
]

const columns: ColumnDef<Payment>[] = [
  {
    accessorKey: "status",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
    cell: ({ row }) => <span className="capitalize">{row.getValue("status")}</span>,
  },
  {
    accessorKey: "email",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
  },
  {
    accessorKey: "amount",
    header: () => <div className="text-right">Amount</div>,
    cell: ({ row }) => {
      const amount = parseFloat(row.getValue("amount"))
      const formatted = new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
      }).format(amount)
      return <div className="text-right font-medium tabular-nums">{formatted}</div>
    },
  },
]

export default function DataTableDemo01() {
  return <DataTable columns={columns} data={data} />
}
```

---

### Row selection

A [Checkbox](/components/checkbox) column with an indeterminate select-all, wired through TanStack's row-selection state and the pagination count.

**Install:**

```bash
npx shadcn@latest add @designrevision/data-table-selection-01
```

**Dependencies:** @designrevision/data-table, checkbox

```tsx
// components/ui/data-table-selection-01.tsx
"use client"

import { type ColumnDef } from "@tanstack/react-table"

import { Checkbox } from "@/components/ui/checkbox"
import { DataTable, DataTableColumnHeader } from "@/components/ui/data-table"

type User = {
  id: string
  name: string
  email: string
  role: "owner" | "member" | "viewer"
}

const data: User[] = [
  { id: "1", name: "Ada Lovelace", email: "ada@example.com", role: "owner" },
  { id: "2", name: "Alan Turing", email: "alan@example.com", role: "member" },
  { id: "3", name: "Grace Hopper", email: "grace@example.com", role: "member" },
  { id: "4", name: "Katherine Johnson", email: "kj@example.com", role: "viewer" },
  { id: "5", name: "Margaret Hamilton", email: "margaret@example.com", role: "member" },
  { id: "6", name: "Hedy Lamarr", email: "hedy@example.com", role: "viewer" },
  { id: "7", name: "Dorothy Vaughan", email: "dorothy@example.com", role: "member" },
  { id: "8", name: "Radia Perlman", email: "radia@example.com", role: "owner" },
  { id: "9", name: "Barbara Liskov", email: "barbara@example.com", role: "member" },
  { id: "10", name: "Frances Allen", email: "frances@example.com", role: "viewer" },
  { id: "11", name: "Shafi Goldwasser", email: "shafi@example.com", role: "member" },
  { id: "12", name: "Carol Shaw", email: "carol@example.com", role: "viewer" },
]

const columns: ColumnDef<User>[] = [
  {
    id: "select",
    header: ({ table }) => (
      <Checkbox
        checked={
          table.getIsAllPageRowsSelected() ||
          (table.getIsSomePageRowsSelected() && "indeterminate")
        }
        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
        aria-label="Select all"
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) => row.toggleSelected(!!value)}
        aria-label="Select row"
      />
    ),
    enableSorting: false,
    enableHiding: false,
  },
  {
    accessorKey: "name",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
    cell: ({ row }) => <span className="font-medium">{row.getValue("name")}</span>,
  },
  {
    accessorKey: "email",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
  },
  {
    accessorKey: "role",
    header: "Role",
    cell: ({ row }) => <span className="capitalize">{row.getValue("role")}</span>,
  },
]

export default function DataTableSelection01() {
  return <DataTable columns={columns} data={data} />
}
```

---

### Filter toolbar

A toolbar with a column-search input and a reset button — column filtering via `getFilteredRowModel`.

**Install:**

```bash
npx shadcn@latest add @designrevision/data-table-filter-01
```

**Dependencies:** lucide-react, @designrevision/data-table, button, input

```tsx
// components/ui/data-table-filter-01.tsx
"use client"

import { type ColumnDef } from "@tanstack/react-table"
import { XIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { DataTable, DataTableColumnHeader } from "@/components/ui/data-table"
import { Input } from "@/components/ui/input"

type Payment = {
  id: string
  amount: number
  status: "pending" | "processing" | "success" | "failed"
  email: string
}

const data: Payment[] = [
  { id: "1", amount: 316, status: "success", email: "ken99@example.com" },
  { id: "2", amount: 242, status: "success", email: "abe45@example.com" },
  { id: "3", amount: 837, status: "processing", email: "monserrat44@example.com" },
  { id: "4", amount: 874, status: "success", email: "silas22@example.com" },
  { id: "5", amount: 721, status: "failed", email: "carmella@example.com" },
  { id: "6", amount: 459, status: "pending", email: "jordan78@example.com" },
  { id: "7", amount: 633, status: "processing", email: "nina@example.com" },
  { id: "8", amount: 192, status: "success", email: "leo@example.com" },
  { id: "9", amount: 548, status: "failed", email: "priya@example.com" },
  { id: "10", amount: 387, status: "pending", email: "marco@example.com" },
  { id: "11", amount: 905, status: "success", email: "dana@example.com" },
  { id: "12", amount: 264, status: "processing", email: "omar@example.com" },
]

const columns: ColumnDef<Payment>[] = [
  {
    accessorKey: "status",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
    cell: ({ row }) => <span className="capitalize">{row.getValue("status")}</span>,
  },
  {
    accessorKey: "email",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
  },
  {
    accessorKey: "amount",
    header: () => <div className="text-right">Amount</div>,
    cell: ({ row }) => {
      const amount = parseFloat(row.getValue("amount"))
      const formatted = new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
      }).format(amount)
      return <div className="text-right font-medium tabular-nums">{formatted}</div>
    },
  },
]

export default function DataTableFilter01() {
  return (
    <DataTable
      columns={columns}
      data={data}
      toolbar={(table) => {
        const isFiltered = table.getState().columnFilters.length > 0
        return (
          <div className="flex items-center gap-2">
            <Input
              placeholder="Filter emails…"
              value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
              onChange={(event) =>
                table.getColumn("email")?.setFilterValue(event.target.value)
              }
              className="h-8 w-[180px] lg:w-[250px]"
            />
            {isFiltered && (
              <Button
                variant="ghost"
                size="sm"
                className="h-8 px-2 lg:px-3"
                onClick={() => table.resetColumnFilters()}
              >
                Reset
                <XIcon />
              </Button>
            )}
          </div>
        )
      }}
    />
  )
}
```

---

### Column visibility

A View-options menu to show and hide columns — built on [Dropdown Menu](/components/dropdown-menu) checkbox items.

**Install:**

```bash
npx shadcn@latest add @designrevision/data-table-visibility-01
```

**Dependencies:** @designrevision/data-table

```tsx
// components/ui/data-table-visibility-01.tsx
"use client"

import { type ColumnDef } from "@tanstack/react-table"

import {
  DataTable,
  DataTableColumnHeader,
  DataTableViewOptions,
} from "@/components/ui/data-table"

type User = {
  id: string
  name: string
  email: string
  role: "owner" | "member" | "viewer"
  team: string
}

const data: User[] = [
  { id: "1", name: "Ada Lovelace", email: "ada@example.com", role: "owner", team: "Engineering" },
  { id: "2", name: "Alan Turing", email: "alan@example.com", role: "member", team: "Research" },
  { id: "3", name: "Grace Hopper", email: "grace@example.com", role: "member", team: "Engineering" },
  { id: "4", name: "Katherine Johnson", email: "kj@example.com", role: "viewer", team: "Research" },
  { id: "5", name: "Margaret Hamilton", email: "margaret@example.com", role: "member", team: "Apollo" },
  { id: "6", name: "Hedy Lamarr", email: "hedy@example.com", role: "viewer", team: "Comms" },
  { id: "7", name: "Dorothy Vaughan", email: "dorothy@example.com", role: "member", team: "Research" },
  { id: "8", name: "Radia Perlman", email: "radia@example.com", role: "owner", team: "Networking" },
  { id: "9", name: "Barbara Liskov", email: "barbara@example.com", role: "member", team: "Engineering" },
  { id: "10", name: "Frances Allen", email: "frances@example.com", role: "viewer", team: "Compilers" },
  { id: "11", name: "Shafi Goldwasser", email: "shafi@example.com", role: "member", team: "Security" },
  { id: "12", name: "Carol Shaw", email: "carol@example.com", role: "viewer", team: "Games" },
]

const columns: ColumnDef<User>[] = [
  {
    accessorKey: "name",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
    cell: ({ row }) => <span className="font-medium">{row.getValue("name")}</span>,
  },
  {
    accessorKey: "email",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
  },
  {
    accessorKey: "role",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Role" />,
    cell: ({ row }) => <span className="capitalize">{row.getValue("role")}</span>,
  },
  {
    accessorKey: "team",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Team" />,
  },
]

export default function DataTableVisibility01() {
  return (
    <DataTable
      columns={columns}
      data={data}
      toolbar={(table) => (
        <div className="flex items-center justify-end">
          <DataTableViewOptions table={table} />
        </div>
      )}
    />
  )
}
```

---

### Faceted filters

A multi-select facet ([Popover](/components/popover) + [Command](/components/command)) with live option counts and selected-value badges — the classic data-table filter.

**Install:**

```bash
npx shadcn@latest add @designrevision/data-table-faceted-01
```

**Dependencies:** lucide-react, @designrevision/data-table

```tsx
// components/ui/data-table-faceted-01.tsx
"use client"

import { type ColumnDef } from "@tanstack/react-table"
import {
  CircleCheckIcon,
  CircleDashedIcon,
  CircleDotIcon,
  CircleIcon,
  CircleXIcon,
} from "lucide-react"

import {
  DataTable,
  DataTableColumnHeader,
  DataTableFacetedFilter,
} from "@/components/ui/data-table"

const statuses = [
  { value: "backlog", label: "Backlog", icon: CircleDashedIcon },
  { value: "todo", label: "Todo", icon: CircleIcon },
  { value: "in progress", label: "In Progress", icon: CircleDotIcon },
  { value: "done", label: "Done", icon: CircleCheckIcon },
  { value: "canceled", label: "Canceled", icon: CircleXIcon },
]

type Task = { id: string; title: string; status: string }

const data: Task[] = [
  { id: "TASK-8782", title: "Refactor the auth middleware", status: "in progress" },
  { id: "TASK-7878", title: "Add dark mode toggle", status: "backlog" },
  { id: "TASK-7839", title: "Fix the pagination off-by-one", status: "done" },
  { id: "TASK-5562", title: "Write the onboarding docs", status: "todo" },
  { id: "TASK-8686", title: "Migrate to the new API", status: "canceled" },
  { id: "TASK-1280", title: "Compress hero images", status: "done" },
  { id: "TASK-7262", title: "Audit the color tokens", status: "in progress" },
  { id: "TASK-1138", title: "Set up the staging env", status: "todo" },
  { id: "TASK-7184", title: "Remove the legacy flags", status: "backlog" },
  { id: "TASK-5160", title: "Add e2e tests for checkout", status: "todo" },
  { id: "TASK-5618", title: "Tune the cache headers", status: "done" },
  { id: "TASK-6699", title: "Draft the Q3 roadmap", status: "in progress" },
]

const columns: ColumnDef<Task>[] = [
  {
    accessorKey: "id",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Task" />,
    cell: ({ row }) => (
      <span className="text-muted-foreground font-mono text-xs">
        {row.getValue("id")}
      </span>
    ),
  },
  {
    accessorKey: "title",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Title" />,
    cell: ({ row }) => <span className="font-medium">{row.getValue("title")}</span>,
  },
  {
    accessorKey: "status",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
    cell: ({ row }) => {
      const status = statuses.find((s) => s.value === row.getValue("status"))
      if (!status) return null
      return (
        <div className="flex items-center gap-2">
          <status.icon className="text-muted-foreground size-4" />
          <span>{status.label}</span>
        </div>
      )
    },
    filterFn: (row, id, value) => (value as string[]).includes(row.getValue(id)),
  },
]

export default function DataTableFaceted01() {
  return (
    <DataTable
      columns={columns}
      data={data}
      toolbar={(table) => (
        <div className="flex items-center gap-2">
          <DataTableFacetedFilter
            column={table.getColumn("status")}
            title="Status"
            options={statuses}
          />
        </div>
      )}
    />
  )
}
```

---

### Row actions

A per-row "⋯" actions menu ([Dropdown Menu](/components/dropdown-menu)) with edit, copy, and a destructive delete.

**Install:**

```bash
npx shadcn@latest add @designrevision/data-table-row-actions-01
```

**Dependencies:** @designrevision/data-table, badge

```tsx
// components/ui/data-table-row-actions-01.tsx
"use client"

import { type ColumnDef } from "@tanstack/react-table"

import { Badge } from "@/components/ui/badge"
import {
  DataTable,
  DataTableColumnHeader,
  DataTableRowActions,
} from "@/components/ui/data-table"

type Task = {
  id: string
  title: string
  type: "Bug" | "Feature" | "Documentation"
  status: "todo" | "in progress" | "done"
}

const data: Task[] = [
  { id: "TASK-8782", title: "Refactor the auth middleware", type: "Bug", status: "in progress" },
  { id: "TASK-7878", title: "Add dark mode toggle", type: "Feature", status: "todo" },
  { id: "TASK-7839", title: "Fix the pagination off-by-one", type: "Bug", status: "done" },
  { id: "TASK-5562", title: "Write the onboarding docs", type: "Documentation", status: "todo" },
  { id: "TASK-1280", title: "Compress hero images", type: "Feature", status: "done" },
  { id: "TASK-7262", title: "Audit the color tokens", type: "Documentation", status: "in progress" },
  { id: "TASK-1138", title: "Set up the staging env", type: "Feature", status: "todo" },
  { id: "TASK-5160", title: "Add e2e tests for checkout", type: "Bug", status: "todo" },
  { id: "TASK-5618", title: "Tune the cache headers", type: "Feature", status: "done" },
  { id: "TASK-6699", title: "Draft the Q3 roadmap", type: "Documentation", status: "in progress" },
  { id: "TASK-2342", title: "Patch the XSS in comments", type: "Bug", status: "todo" },
  { id: "TASK-9081", title: "Localize the checkout flow", type: "Feature", status: "todo" },
]

const columns: ColumnDef<Task>[] = [
  {
    accessorKey: "id",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Task" />,
    cell: ({ row }) => (
      <span className="text-muted-foreground font-mono text-xs">
        {row.getValue("id")}
      </span>
    ),
  },
  {
    accessorKey: "title",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Title" />,
    cell: ({ row }) => (
      <div className="flex items-center gap-2">
        <Badge variant="outline">{row.original.type}</Badge>
        <span className="font-medium">{row.getValue("title")}</span>
      </div>
    ),
  },
  {
    accessorKey: "status",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
    cell: ({ row }) => <span className="capitalize">{row.getValue("status")}</span>,
  },
  {
    id: "actions",
    cell: ({ row }) => (
      <div className="text-right">
        <DataTableRowActions row={row} />
      </div>
    ),
    enableSorting: false,
    enableHiding: false,
  },
]

export default function DataTableRowActions01() {
  return <DataTable columns={columns} data={data} />
}
```

---

### Full tasks table

The complete showcase — selection, sortable headers, status & priority faceted filters, global search, column visibility, row actions, and pagination together.

**Install:**

```bash
npx shadcn@latest add @designrevision/data-table-full-01
```

**Dependencies:** lucide-react, @designrevision/data-table, badge, checkbox, input, button

```tsx
// components/ui/data-table-full-01.tsx
"use client"

import { type ColumnDef } from "@tanstack/react-table"
import {
  ArrowDownIcon,
  ArrowRightIcon,
  ArrowUpIcon,
  CircleCheckIcon,
  CircleDashedIcon,
  CircleDotIcon,
  CircleIcon,
  CircleXIcon,
  XIcon,
} from "lucide-react"

import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
  DataTable,
  DataTableColumnHeader,
  DataTableFacetedFilter,
  DataTableRowActions,
  DataTableViewOptions,
} from "@/components/ui/data-table"
import { Input } from "@/components/ui/input"

const statuses = [
  { value: "backlog", label: "Backlog", icon: CircleDashedIcon },
  { value: "todo", label: "Todo", icon: CircleIcon },
  { value: "in progress", label: "In Progress", icon: CircleDotIcon },
  { value: "done", label: "Done", icon: CircleCheckIcon },
  { value: "canceled", label: "Canceled", icon: CircleXIcon },
]

const priorities = [
  { value: "low", label: "Low", icon: ArrowDownIcon },
  { value: "medium", label: "Medium", icon: ArrowRightIcon },
  { value: "high", label: "High", icon: ArrowUpIcon },
]

type Task = {
  id: string
  title: string
  type: "Bug" | "Feature" | "Documentation"
  status: string
  priority: string
}

const data: Task[] = [
  { id: "TASK-8782", title: "Refactor the auth middleware", type: "Bug", status: "in progress", priority: "high" },
  { id: "TASK-7878", title: "Add dark mode toggle", type: "Feature", status: "backlog", priority: "medium" },
  { id: "TASK-7839", title: "Fix the pagination off-by-one", type: "Bug", status: "done", priority: "high" },
  { id: "TASK-5562", title: "Write the onboarding docs", type: "Documentation", status: "todo", priority: "low" },
  { id: "TASK-8686", title: "Migrate to the new API", type: "Feature", status: "canceled", priority: "medium" },
  { id: "TASK-1280", title: "Compress hero images", type: "Feature", status: "done", priority: "low" },
  { id: "TASK-7262", title: "Audit the color tokens", type: "Documentation", status: "in progress", priority: "medium" },
  { id: "TASK-1138", title: "Set up the staging env", type: "Feature", status: "todo", priority: "high" },
  { id: "TASK-7184", title: "Remove the legacy flags", type: "Bug", status: "backlog", priority: "low" },
  { id: "TASK-5160", title: "Add e2e tests for checkout", type: "Bug", status: "todo", priority: "high" },
  { id: "TASK-5618", title: "Tune the cache headers", type: "Feature", status: "done", priority: "medium" },
  { id: "TASK-6699", title: "Draft the Q3 roadmap", type: "Documentation", status: "in progress", priority: "medium" },
  { id: "TASK-2342", title: "Patch the XSS in comments", type: "Bug", status: "todo", priority: "high" },
  { id: "TASK-9081", title: "Localize the checkout flow", type: "Feature", status: "todo", priority: "low" },
  { id: "TASK-4123", title: "Add rate limiting to the API", type: "Feature", status: "backlog", priority: "medium" },
  { id: "TASK-3377", title: "Document the deploy runbook", type: "Documentation", status: "done", priority: "low" },
]

const columns: ColumnDef<Task>[] = [
  {
    id: "select",
    header: ({ table }) => (
      <Checkbox
        checked={
          table.getIsAllPageRowsSelected() ||
          (table.getIsSomePageRowsSelected() && "indeterminate")
        }
        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
        aria-label="Select all"
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) => row.toggleSelected(!!value)}
        aria-label="Select row"
      />
    ),
    enableSorting: false,
    enableHiding: false,
  },
  {
    accessorKey: "id",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Task" />,
    cell: ({ row }) => (
      <span className="text-muted-foreground font-mono text-xs">
        {row.getValue("id")}
      </span>
    ),
    enableHiding: false,
  },
  {
    accessorKey: "title",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Title" />,
    cell: ({ row }) => (
      <div className="flex items-center gap-2">
        <Badge variant="outline">{row.original.type}</Badge>
        <span className="max-w-[320px] truncate font-medium">
          {row.getValue("title")}
        </span>
      </div>
    ),
  },
  {
    accessorKey: "status",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
    cell: ({ row }) => {
      const status = statuses.find((s) => s.value === row.getValue("status"))
      if (!status) return null
      return (
        <div className="flex items-center gap-2">
          <status.icon className="text-muted-foreground size-4" />
          <span>{status.label}</span>
        </div>
      )
    },
    filterFn: (row, id, value) => (value as string[]).includes(row.getValue(id)),
  },
  {
    accessorKey: "priority",
    header: ({ column }) => <DataTableColumnHeader column={column} title="Priority" />,
    cell: ({ row }) => {
      const priority = priorities.find((p) => p.value === row.getValue("priority"))
      if (!priority) return null
      return (
        <div className="flex items-center gap-2">
          <priority.icon className="text-muted-foreground size-4" />
          <span>{priority.label}</span>
        </div>
      )
    },
    filterFn: (row, id, value) => (value as string[]).includes(row.getValue(id)),
  },
  {
    id: "actions",
    cell: ({ row }) => (
      <div className="text-right">
        <DataTableRowActions row={row} />
      </div>
    ),
    enableSorting: false,
    enableHiding: false,
  },
]

export default function DataTableFull01() {
  return (
    <DataTable
      columns={columns}
      data={data}
      toolbar={(table) => {
        const isFiltered = table.getState().columnFilters.length > 0
        return (
          <div className="flex flex-wrap items-center gap-2">
            <Input
              placeholder="Filter tasks…"
              value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
              onChange={(event) =>
                table.getColumn("title")?.setFilterValue(event.target.value)
              }
              className="h-8 w-[150px] lg:w-[250px]"
            />
            <DataTableFacetedFilter
              column={table.getColumn("status")}
              title="Status"
              options={statuses}
            />
            <DataTableFacetedFilter
              column={table.getColumn("priority")}
              title="Priority"
              options={priorities}
            />
            {isFiltered && (
              <Button
                variant="ghost"
                size="sm"
                className="h-8 px-2 lg:px-3"
                onClick={() => table.resetColumnFilters()}
              >
                Reset
                <XIcon />
              </Button>
            )}
            <DataTableViewOptions table={table} />
          </div>
        )
      }}
    />
  )
}
```
