# Data Table

Powerful table and datagrids built using TanStack Table.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).



<ComponentPreview name="data-table-demo" align="start" previewClassName="items-start h-auto px-4 md:px-8" />

## Introduction [#introduction]

Every data table or datagrid I've created has been unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources.

It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that [headless UI](https://tanstack.com/table/latest/docs/overview#what-is-headless-ui) provides.

So instead of a data-table component, I thought it would be more helpful to provide a guide on how to build your own.

We'll start with the basic `<Table />` component and build a complex data table from scratch.

<Callout className="mt-4">
  **Tip:** If you find yourself using the same table in multiple places in your app, you can always extract it into a reusable component.
</Callout>

## Table of Contents [#table-of-contents]

This guide will show you how to use [TanStack Table](https://tanstack.com/table) and the `<Table />` component to build your own custom data table. We'll cover the following topics:

* [Set up Table Features](#set-up-table-features)
* [Basic Table](#basic-table)
* [Row Actions](#row-actions)
* [Pagination](#pagination)
* [Sorting](#sorting)
* [Filtering](#filtering)
* [Visibility](#visibility)
* [Row Selection](#row-selection)
* [Reusable Components](#reusable-components)

## Installation [#installation]

1. Add the `<Table />` component to your project:

```bash
npx shadcn@latest add table
```

2. Add the `@tanstack/react-table` dependency. This guide uses **TanStack Table v9**:

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

## Prerequisites [#prerequisites]

We are going to build a table to show recent payments. Here's what our data looks like:

```tsx showLineNumbers
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",
  },
  // ...
];
```

## Project Structure [#project-structure]

Start by creating the following file structure:

```txt
app
└── payments
    ├── columns.tsx
    ├── data-table-features.ts
    ├── data-table.tsx
    └── page.tsx
```

I'm using a Next.js example here but this works for any other React framework.

* `columns.tsx` (client component) will contain our column definitions.
* `data-table-features.ts` will contain the shared `features` object that tells TanStack Table which behavior to enable.
* `data-table.tsx` (client component) will contain our `<DataTable />` component.
* `page.tsx` (server component) is where we'll fetch data and render our table.

## Set up Table Features [#set-up-table-features]

TanStack Table v9 is feature-based: you opt into the behavior you want — sorting, filtering, pagination, and so on — by declaring it with `tableFeatures()`. Anything you don't list is tree-shaken out of your bundle. That includes the built-in filter and sort functions: register the ones your columns rely on under `filterFns` and `sortFns` (our email filter uses `includesString`, and string columns sort with `text` / `alphanumeric`).

```tsx showLineNumbers title="app/payments/data-table-features.ts"
import {
  columnFilteringFeature,
  columnVisibilityFeature,
  createFilteredRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  filterFn_includesString,
  rowPaginationFeature,
  rowSelectionFeature,
  rowSortingFeature,
  sortFn_alphanumeric,
  sortFn_text,
  tableFeatures,
} from "@tanstack/react-table";

// New in v9: declare the features this table uses — anything you don't
// register is tree-shaken out of the bundle.
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 },
});

// Pass this as the first generic argument to `ColumnDef`, `Column`, `Table`,
// and `Row` so each type knows which feature APIs are available.
export type DataTableFeatures = typeof features;
```

<Callout className="mt-4">
  **Note:** The core row model is always included, so you never register it yourself. Row models for optional features are created with `create*RowModel()` and registered on the features object — there are no more `get*RowModel` table options.
</Callout>

## Basic Table [#basic-table]

Let's start by building a basic table.

<Steps>
  ### Column Definitions [#column-definitions]

  First, we'll define our columns.

  ```tsx showLineNumbers title="app/payments/columns.tsx" {3,5,16-17,19-29}
  "use client";

  import { createColumnHelper } from "@tanstack/react-table";

  import { type DataTableFeatures } from "./data-table-features";

  // This type is used to define the shape of our data.
  // You can use a Zod schema here if you want.
  export type Payment = {
    id: string;
    amount: number;
    status: "pending" | "processing" | "success" | "failed";
    email: string;
  };

  // Use `accessor` for data columns and `display` for columns without one.
  const columnHelper = createColumnHelper<DataTableFeatures, Payment>();

  export const columns = columnHelper.columns([
    columnHelper.accessor("status", {
      header: "Status",
    }),
    columnHelper.accessor("email", {
      header: "Email",
    }),
    columnHelper.accessor("amount", {
      header: "Amount",
    }),
  ]);
  ```

  <Callout className="mt-4">
    **Note:** Columns are where you define the core of what your table
    will look like. They define the data that will be displayed, how it will be
    formatted, sorted and filtered.
  </Callout>

  ### `<DataTable />` component [#datatable--component]

  Next, we'll create a `<DataTable />` component to render our table.

  ```tsx showLineNumbers title="app/payments/data-table.tsx"
  "use client";

  import { useTable, type ColumnDef, type RowData } from "@tanstack/react-table";

  import {
    Table,
    TableBody,
    TableCell,
    TableHead,
    TableHeader,
    TableRow,
  } from "@/components/ui/table";

  import { features, type DataTableFeatures } from "./data-table-features";

  interface DataTableProps<TData extends RowData> {
    columns: ColumnDef<DataTableFeatures, TData>[];
    data: TData[];
  }

  export function DataTable<TData extends RowData>({
    columns,
    data,
  }: DataTableProps<TData>) {
    const table = useTable({
      features,
      data,
      columns,
    });

    return (
      <div className="overflow-hidden rounded-md border">
        <Table>
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => {
                  return (
                    <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"}
                >
                  {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>
    );
  }
  ```

  <Callout>
    **Tip**: If you find yourself using `<DataTable />` in multiple places, this is the component you could make reusable by extracting it to `components/ui/data-table.tsx`.

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

  <Callout className="mt-4">
    **`<table.FlexRender />` vs `flexRender`:** This guide uses v9's `<table.FlexRender header={header} />` and `<table.FlexRender cell={cell} />` component, available right on the table instance — no extra import needed. The classic `flexRender(component, context)` helper from v8 still works too, if you prefer the function form (or need to render outside the component that owns `table`, where you can also import the standalone `<FlexRender />`).
  </Callout>

  ### Render the table [#render-the-table]

  Finally, we'll render our table in our page component.

  ```tsx showLineNumbers title="app/payments/page.tsx" {22}
  import { columns, Payment } from "./columns";
  import { DataTable } from "./data-table";

  async function getData(): Promise<Payment[]> {
    // Fetch data from your API here.
    return [
      {
        id: "728ed52f",
        amount: 100,
        status: "pending",
        email: "m@example.com",
      },
      // ...
    ];
  }

  export default async function DemoPage() {
    const data = await getData();

    return (
      <div className="container mx-auto py-10">
        <DataTable columns={columns} data={data} />
      </div>
    );
  }
  ```
</Steps>

## Cell Formatting [#cell-formatting]

Let's format the amount cell to display the dollar amount. We'll also align the cell to the right.

<Steps>
  ### Update columns definition [#update-columns-definition]

  Update the `header` and `cell` definitions for amount as follows:

  ```tsx showLineNumbers title="app/payments/columns.tsx" {3-13}
  export const columns = columnHelper.columns([
    columnHelper.accessor("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">{formatted}</div>;
      },
    }),
  ]);
  ```

  You can use the same approach to format other cells and headers.
</Steps>

## Row Actions [#row-actions]

Let's add row actions to our table. We'll use a `<DropdownMenu />` component for this.

<Steps>
  ### Update columns definition [#update-columns-definition-1]

  Update our columns definition to add a new `actions` column. The `actions` cell returns a `<DropdownMenu />` component.

  ```tsx showLineNumbers title="app/payments/columns.tsx" {4,6-14,18-45}
  "use client";

  import { createColumnHelper } from "@tanstack/react-table";
  import { MoreHorizontal } from "lucide-react";

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

  export const columns = columnHelper.columns([
    // ...
    columnHelper.display({
      id: "actions",
      cell: ({ row }) => {
        const payment = row.original;

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

  You can access the row data using `row.original` in the `cell` function. Use this to handle actions for your row eg. use the `id` to make a DELETE call to your API.
</Steps>

## Pagination [#pagination]

Next, we'll add pagination to our table.

<Steps>
  ### Pagination is already enabled [#pagination-is-already-enabled]

  Because our features object includes `rowPaginationFeature` and `createPaginatedRowModel()`, the table automatically paginates rows into pages of 10 — there's nothing to add to `useTable`. See the [pagination docs](https://tanstack.com/table/latest/docs/framework/react/guide/pagination) for more information on customizing page size and implementing manual pagination.

  ### Add pagination controls [#add-pagination-controls]

  We can add pagination controls to our table using the `<Button />` component and the `table.previousPage()`, `table.nextPage()` API methods.

  ```tsx showLineNumbers title="app/payments/data-table.tsx" {1,20-37}
  import { Button } from "@/components/ui/button"

  export function DataTable<TData extends RowData>({
    columns,
    data,
  }: DataTableProps<TData>) {
    const table = useTable({
      features,
      data,
      columns,
    })

    return (
      <div>
        <div className="overflow-hidden rounded-md border">
          <Table>
            { // .... }
          </Table>
        </div>
        <div className="flex items-center justify-end space-x-2 py-4">
          <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>
        </div>
      </div>
    )
  }
  ```

  See [Reusable Components](#reusable-components) section for a more advanced pagination component.
</Steps>

## Sorting [#sorting]

Let's make the email column sortable.

The `rowSortingFeature` and sorted row model are already registered in our features object, so all that's left is wiring up the sorting state.

<Steps>
  ### Update `<DataTable>` [#update-datatable]

  ```tsx showLineNumbers title="app/payments/data-table.tsx" {3,8,15,21-24}
  "use client"

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

  export function DataTable<TData extends RowData>({
    columns,
    data,
  }: DataTableProps<TData>) {
    const [sorting, setSorting] = React.useState<SortingState>([])

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

    return (
      <div>
        <div className="overflow-hidden rounded-md border">
          <Table>{ ... }</Table>
        </div>
      </div>
    )
  }
  ```

  ### Make header cell sortable [#make-header-cell-sortable]

  We can now update the `email` header cell to add sorting controls.

  ```tsx showLineNumbers title="app/payments/columns.tsx" {4,8-18}
  "use client";

  import { createColumnHelper } from "@tanstack/react-table";
  import { ArrowUpDown } from "lucide-react";

  export const columns = columnHelper.columns([
    columnHelper.accessor("email", {
      header: ({ column }) => {
        return (
          <Button
            variant="ghost"
            onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
          >
            Email
            <ArrowUpDown className="ml-2 h-4 w-4" />
          </Button>
        );
      },
    }),
  ]);
  ```

  This will automatically sort the table (asc and desc) when the user toggles on the header cell.
</Steps>

## Filtering [#filtering]

Let's add a search input to filter emails in our table.

The `columnFilteringFeature` and filtered row model are already part of our features object, so we only need to wire up the filter state and render an input.

<Steps>
  ### Update `<DataTable>` [#update-datatable-1]

  ```tsx showLineNumbers title="app/payments/data-table.tsx" {7,13,20-22,29,32,38-46}
  "use client"

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

  import { Button } from "@/components/ui/button"
  import { Input } from "@/components/ui/input"

  export function DataTable<TData extends RowData>({
    columns,
    data,
  }: DataTableProps<TData>) {
    const [sorting, setSorting] = React.useState<SortingState>([])
    const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
      []
    )

    const table = useTable({
      features,
      data,
      columns,
      onSortingChange: setSorting,
      onColumnFiltersChange: setColumnFilters,
      state: {
        sorting,
        columnFilters,
      },
    })

    return (
      <div>
        <div className="flex items-center py-4">
          <Input
            placeholder="Filter emails..."
            value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
            onChange={(event) =>
              table.getColumn("email")?.setFilterValue(event.target.value)
            }
            className="max-w-sm"
          />
        </div>
        <div className="overflow-hidden rounded-md border">
          <Table>{ ... }</Table>
        </div>
      </div>
    )
  }
  ```

  Filtering is now enabled for the `email` column. You can add filters to other columns as well. See the [filtering docs](https://tanstack.com/table/latest/docs/framework/react/guide/column-filtering) for more information on customizing filters.
</Steps>

## Visibility [#visibility]

Adding column visibility is fairly simple using `@tanstack/react-table` visibility API.

<Steps>
  ### Update `<DataTable>` [#update-datatable-2]

  ```tsx showLineNumbers title="app/payments/data-table.tsx" {8,14-19,29-30,38,42,57-84}
  "use client"

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

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

  export function DataTable<TData extends RowData>({
    columns,
    data,
  }: DataTableProps<TData>) {
    const [sorting, setSorting] = React.useState<SortingState>([])
    const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
      []
    )
    const [columnVisibility, setColumnVisibility] =
      React.useState<ColumnVisibilityState>({})

    const table = useTable({
      features,
      data,
      columns,
      onSortingChange: setSorting,
      onColumnFiltersChange: setColumnFilters,
      onColumnVisibilityChange: setColumnVisibility,
      state: {
        sorting,
        columnFilters,
        columnVisibility,
      },
    })

    return (
      <div>
        <div className="flex items-center py-4">
          <Input
            placeholder="Filter emails..."
            value={table.getColumn("email")?.getFilterValue() as string}
            onChange={(event) =>
              table.getColumn("email")?.setFilterValue(event.target.value)
            }
            className="max-w-sm"
          />
          <DropdownMenu>
            <DropdownMenuTrigger render={<Button variant="outline" className="ml-auto" />}>
              Columns
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end">
              {table
                .getAllColumns()
                .filter(
                  (column) => column.getCanHide()
                )
                .map((column) => {
                  return (
                    <DropdownMenuCheckboxItem
                      key={column.id}
                      className="capitalize"
                      checked={column.getIsVisible()}
                      onCheckedChange={(value) =>
                        column.toggleVisibility(!!value)
                      }
                    >
                      {column.id}
                    </DropdownMenuCheckboxItem>
                  )
                })}
            </DropdownMenuContent>
          </DropdownMenu>
        </div>
        <div className="overflow-hidden rounded-md border">
          <Table>{ ... }</Table>
        </div>
      </div>
    )
  }
  ```

  This adds a dropdown menu that you can use to toggle column visibility.
</Steps>

## Row Selection [#row-selection]

Next, we're going to add row selection to our table.

<Steps>
  ### Update column definitions [#update-column-definitions]

  ```tsx showLineNumbers title="app/payments/columns.tsx" {6,9-30}
  "use client";

  import { createColumnHelper } from "@tanstack/react-table";

  import { Badge } from "@/components/ui/badge";
  import { Checkbox } from "@/components/ui/checkbox";

  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,
    }),
  ]);
  ```

  ### Update `<DataTable>` [#update-datatable-3]

  ```tsx showLineNumbers title="app/payments/data-table.tsx" {11,20,25}
  export function DataTable<TData extends RowData>({
    columns,
    data,
  }: 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({});

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

    return (
      <div>
        <div className="overflow-hidden rounded-md border">
          <Table />
        </div>
      </div>
    );
  }
  ```

  This adds a checkbox to each row and a checkbox in the header to select all rows.

  ### Show selected rows [#show-selected-rows]

  You can show the number of selected rows using the `table.getFilteredSelectedRowModel()` API.

  ```tsx
  <div className="flex-1 text-sm text-muted-foreground">
    {table.getFilteredSelectedRowModel().rows.length} of{" "}
    {table.getFilteredRowModel().rows.length} row(s) selected.
  </div>
  ```
</Steps>

## Reusable Components [#reusable-components]

Here are some components you can use to build your data tables. This is from the [Tasks](/examples/tasks) demo, which shares its features object (and the matching `TasksTableFeatures` type) across every component via a `data-table-features.ts` module — the same pattern we set up in [Set up Table Features](#set-up-table-features).

### Column header [#column-header]

Make any column header sortable and hideable.

<ComponentSource src="/app/(app)/examples/tasks/components/data-table-column-header.tsx" title="data-table.tsx" />

```tsx showLineNumbers {4}
export const columns = columnHelper.columns([
  columnHelper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
]);
```

### Pagination [#pagination-1]

Add pagination controls to your table including page size and selection count.

<ComponentSource src="/app/(app)/examples/tasks/components/data-table-pagination.tsx" title="data-table.tsx" />

```tsx
<DataTablePagination table={table} />
```

### Column toggle [#column-toggle]

A component to toggle column visibility.

<ComponentSource src="/app/(app)/examples/tasks/components/data-table-view-options.tsx" title="data-table.tsx" />

```tsx
<DataTableViewOptions table={table} />
```

## RTL [#rtl]

To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).

<ComponentPreview name="data-table-rtl" direction="rtl" previewClassName="items-start h-auto px-4 md:px-8" />
