# Data Table

Powerful table and datagrids built using TanStack Table.

## Installation

```bash
npx shadcn@latest add https://reactdocs.canceydejean.dev/r/data-table.json
```

[Registry JSON](https://reactdocs.canceydejean.dev/r/data-table.json)

## Preview

```tsx
import { DataTable } from "@/components/ui/data-table/data-table";
import { columns, payments } from "@/components/ui/payments/payments";

export function Preview() {
  return <DataTable columns={columns} data={payments} initialPageSize={5} />;
}
```


## Source

### data-table.tsx

```tsx
"use client";

import {
  useTable,
  type ColumnDef,
  type ColumnFiltersState,
  type ColumnVisibilityState,
  type RowData,
  type RowSelectionState,
  type SortingState,
} from "@tanstack/react-table";
import * as React from "react";

import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { cn } from "cn";

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table/table";
import { features, type DataTableFeatures } from "./data-table-features";

export type { DataTableFeatures };

export interface DataTableProps<TData extends RowData> {
  columns: ColumnDef<DataTableFeatures, TData>[];
  data: TData[];
  filterColumnId?: string;
  filterPlaceholder?: string;
  className?: string;
  showToolbar?: boolean;
  showPagination?: boolean;
  showSelectionCount?: boolean;
  showColumnVisibility?: boolean;
  initialPageSize?: number;
}

export function DataTable<TData extends RowData>({
  columns,
  data,
  filterColumnId = "email",
  filterPlaceholder = "Filter emails...",
  className,
  showToolbar = true,
  showPagination = true,
  showSelectionCount = true,
  showColumnVisibility = true,
  initialPageSize = 10,
}: DataTableProps<TData>) {
  const [sorting, setSorting] = React.useState<SortingState>([]);
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
    [],
  );
  const [columnVisibility, setColumnVisibility] =
    React.useState<ColumnVisibilityState>({});
  const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});

  const table = useTable({
    features,
    data,
    columns,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    initialState: {
      pagination: {
        pageIndex: 0,
        pageSize: initialPageSize,
      },
    },
    state: {
      sorting,
      columnFilters,
      columnVisibility,
      rowSelection,
    },
  });

  const filterColumn = filterColumnId
    ? table.getColumn(filterColumnId)
    : undefined;
  const hideableColumns = table
    .getAllColumns()
    .filter((column) => column.getCanHide());
  const filterValue = filterColumn?.getFilterValue();
  const filterInputValue = typeof filterValue === "string" ? filterValue : "";

  return (
    <div className={cn("w-full", className)}>
      {showToolbar ? (
        <div className="flex items-center gap-2 py-4">
          {filterColumn ? (
            <Input
              placeholder={filterPlaceholder}
              value={filterInputValue}
              onChange={(event) =>
                filterColumn.setFilterValue(event.target.value)
              }
              className="max-w-sm"
            />
          ) : null}
          {showColumnVisibility && hideableColumns.length > 0 ? (
            <DropdownMenu>
              <DropdownMenuTrigger
                render={<Button variant="outline" className="ml-auto" />}
              >
                Columns
              </DropdownMenuTrigger>
              <DropdownMenuContent align="end">
                {hideableColumns.map((column) => (
                  <DropdownMenuCheckboxItem
                    key={column.id}
                    className="capitalize"
                    checked={column.getIsVisible()}
                    onCheckedChange={(value) => column.toggleVisibility(value)}
                  >
                    {column.id}
                  </DropdownMenuCheckboxItem>
                ))}
              </DropdownMenuContent>
            </DropdownMenu>
          ) : null}
        </div>
      ) : null}

      <div className="overflow-hidden rounded-md border">
        <Table>
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => (
                  <TableHead key={header.id}>
                    {header.isPlaceholder ? null : (
                      <table.FlexRender header={header} />
                    )}
                  </TableHead>
                ))}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {table.getRowModel().rows.length ? (
              table.getRowModel().rows.map((row) => (
                <TableRow
                  key={row.id}
                  data-state={row.getIsSelected() ? "selected" : undefined}
                >
                  {row.getVisibleCells().map((cell) => (
                    <TableCell key={cell.id}>
                      <table.FlexRender cell={cell} />
                    </TableCell>
                  ))}
                </TableRow>
              ))
            ) : (
              <TableRow>
                <TableCell
                  colSpan={columns.length}
                  className="h-24 text-center"
                >
                  No results.
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>
      </div>

      {showPagination || showSelectionCount ? (
        <div className="flex items-center justify-end space-x-2 py-4">
          {showSelectionCount ? (
            <div className="text-muted-foreground flex-1 text-sm">
              {table.getFilteredSelectedRowModel().rows.length} of{" "}
              {table.getFilteredRowModel().rows.length} row(s) selected.
            </div>
          ) : null}
          {showPagination ? (
            <>
              <Button
                variant="outline"
                size="sm"
                onClick={() => table.previousPage()}
                disabled={!table.getCanPreviousPage()}
              >
                Previous
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() => table.nextPage()}
                disabled={!table.getCanNextPage()}
              >
                Next
              </Button>
            </>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}
```


### data-table-features.ts

```ts
import {
  columnFilteringFeature,
  columnVisibilityFeature,
  createFilteredRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  filterFn_includesString,
  rowPaginationFeature,
  rowSelectionFeature,
  rowSortingFeature,
  sortFn_alphanumeric,
  sortFn_text,
  tableFeatures,
} from "@tanstack/react-table";

// Features this data table uses — anything not registered is tree-shaken out.
export const features = tableFeatures({
  columnFilteringFeature,
  columnVisibilityFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  rowSortingFeature,
  filteredRowModel: createFilteredRowModel(),
  paginatedRowModel: createPaginatedRowModel(),
  sortedRowModel: createSortedRowModel(),
  filterFns: { includesString: filterFn_includesString },
  sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text },
});

export type DataTableFeatures = typeof features;
```


### payments.tsx

```tsx
"use client";

import { IconArrowsSort, IconDots } from "@tabler/icons-react";
import { createColumnHelper } from "@tanstack/react-table";

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

import { Checkbox } from "@/components/ui/checkbox/checkbox";
import type { DataTableFeatures } from "./data-table-features";

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

export const payments: Payment[] = [
  { id: "728ed52f", amount: 100, status: "pending", email: "m@example.com" },
  {
    id: "489e1d42",
    amount: 125,
    status: "processing",
    email: "example@gmail.com",
  },
  {
    id: "a1b2c3d4",
    amount: 316,
    status: "success",
    email: "ken99@example.com",
  },
  {
    id: "e5f6g7h8",
    amount: 242,
    status: "success",
    email: "Abe45@example.com",
  },
  {
    id: "i9j0k1l2",
    amount: 837,
    status: "processing",
    email: "Monserrat44@example.com",
  },
  {
    id: "m3n4o5p6",
    amount: 874,
    status: "success",
    email: "Silas22@example.com",
  },
  {
    id: "q7r8s9t0",
    amount: 721,
    status: "failed",
    email: "carmella@example.com",
  },
  { id: "u1v2w3x4", amount: 450, status: "pending", email: "sara@example.com" },
  { id: "y5z6a7b8", amount: 199, status: "success", email: "noah@example.com" },
  {
    id: "c9d0e1f2",
    amount: 512,
    status: "processing",
    email: "lia@example.com",
  },
  { id: "g3h4i5j6", amount: 88, status: "failed", email: "omar@example.com" },
  { id: "k7l8m9n0", amount: 1020, status: "success", email: "ava@example.com" },
];

const statusVariant: Record<
  Payment["status"],
  "default" | "secondary" | "destructive" | "outline"
> = {
  pending: "outline",
  processing: "secondary",
  success: "default",
  failed: "destructive",
};

const columnHelper = createColumnHelper<DataTableFeatures, Payment>();

export const columns = columnHelper.columns([
  columnHelper.display({
    id: "select",
    header: ({ table }) => (
      <Checkbox
        checked={table.getIsAllPageRowsSelected()}
        indeterminate={table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()}
        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,
  }),
  columnHelper.accessor("status", {
    header: "Status",
    cell: ({ row }) => (
      <Badge variant={statusVariant[row.original.status]} className="capitalize">
        {row.original.status}
      </Badge>
    ),
  }),
  columnHelper.accessor("email", {
    header: ({ column }) => (
      <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
        Email
        <IconArrowsSort className="ml-2 size-4" />
      </Button>
    ),
    cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>,
    filterFn: "includesString",
  }),
  columnHelper.accessor("amount", {
    header: () => <div className="text-right">Amount</div>,
    cell: ({ row }) => {
      const amount = Number(row.getValue("amount"));
      const formatted = new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
      }).format(amount);

      return <div className="text-right font-medium">{formatted}</div>;
    },
  }),
  columnHelper.display({
    id: "actions",
    enableHiding: false,
    cell: ({ row }) => {
      const payment = row.original;

      return (
        <DropdownMenu>
          <DropdownMenuTrigger render={<Button variant="ghost" className="size-8 p-0" />}>
            <span className="sr-only">Open menu</span>
            <IconDots className="size-4" />
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuLabel>Actions</DropdownMenuLabel>
            <DropdownMenuItem
              onClick={() => {
                void navigator.clipboard.writeText(payment.id);
              }}
            >
              Copy payment ID
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem>View customer</DropdownMenuItem>
            <DropdownMenuItem>View payment details</DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      );
    },
  }),
]);
```



## Usage

Build interactive data tables with sorting, filtering, pagination, column visibility, and row
selection on top of TanStack Table **v9** and the [Table](/components/table) primitives.

```tsx
import { DataTable } from "@/components/ui/data-table";
import { columns, payments } from "@/components/ui/payments";

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

`payments.tsx` is a sample column definition and dataset you can copy and adapt. Declare which
TanStack features you need in `data-table-features.ts` (`tableFeatures(...)`). Only registered
features ship in the bundle. Pass `DataTableFeatures` as the first generic to `createColumnHelper`
and `ColumnDef`.

