Docs
Data Table

Data Table

A comprehensive guide to building powerful, interactive tables and datagrids using the headless TanStack Table V8 library and styled components.

Loading...

Introduction: Building Your Own Data Table

Data tables and datagrids are fundamental UI patterns, but their specific implementations often vary significantly based on project needs. Requirements for sorting, filtering, pagination, row actions, and data sources differ greatly from one application to another.

Attempting to create a single, monolithic data table component that caters to every possible variation would inevitably lead to excessive complexity and compromise the flexibility offered by headless UI libraries like TanStack Table. Headless UI provides the logic and state management, leaving the rendering and styling entirely up to you.

Therefore, instead of offering a pre-built <DataTable /> component, this documentation provides a step-by-step guide on constructing your own feature-rich data tables. We will leverage the foundational <Table /> component primitives (like <Table>, <TableRow>, <TableCell>, etc.) alongside the powerful TanStack Table V8 library to build a sophisticated data table from the ground up, demonstrating how to integrate various features progressively.

Guide Overview: Table of Contents

This comprehensive guide demonstrates how to effectively utilize the TanStack Table V8 library in conjunction with our styled <Table /> components (<Table>, <TableHeader>, <TableBody>, <TableRow>, <TableHead>, <TableCell>) to construct custom, feature-rich data tables tailored to your specific needs. We will explore the following key areas:

Required Installation Steps

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

npx shadcn@latest add table
  1. Add tanstack/react-table dependency:

npm install @tanstack/react-table

Data Structure Prerequisites

Before we begin building the table, let's define the structure of the data we'll be working with. For this guide, we will create a table displaying a list of recent payments. The data for each payment will adhere to the following TypeScript type definition:

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

Recommended Project Structure

To organize our data table implementation effectively, we recommend establishing the following file structure within your application (using Next.js App Router conventions as an example, but adaptable to other frameworks):

app
└── payments
    ├── columns.tsx
    ├── data-table.tsx
    └── page.tsx

While this example uses Next.js conventions, the principles apply broadly to other React frameworks.

  • columns.tsx: (Client Component) This file will house the column definitions for TanStack Table. It dictates how data is accessed, formatted, and potentially sorted/filtered for each column.

  • data-table.tsx: (Client Component) This component will encapsulate the core TanStack Table logic and render the actual <Table> structure using our UI components. It will receive columns and data as props.

  • page.tsx: (Server Component, in Next.js) This is the route component where data fetching typically occurs. It will import and render the data-table.tsx component, passing the fetched data and column definitions to it.

Building the Basic Table Structure

Let's begin by constructing the fundamental table display, rendering our payment data with the defined columns.

Step 1: Defining Table Columns

The first step is to define the columns for our table using TanStack Table's ColumnDef type. These definitions tell the table which data properties to display and what header text to use.

"use client";

// 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;
};

export const columns: ColumnDef<Payment>[] = [
  {
    accessorKey: "status",
    header: "Status",
  },
  {
    accessorKey: "email",
    header: "Email",
  },
  {
    accessorKey: "amount",
    header: "Amount",
  },
];

Step 2: Creating the <DataTable /> Component

Next, we create the reusable DataTable component. This component initializes TanStack Table using the useReactTable hook, passing in the data and column definitions. It then iterates over the table model provided by the hook to render the table structure using our styled <Table>, <TableHeader>, <TableRow>, etc., components.

"use client";

import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table";

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

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[];
  data: TData[];
}

export function DataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <div className="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
                      : 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>
  );
}

Step 3: Rendering the Table in Your Page

Finally, import the columns and the DataTable component into your page component (e.g., page.tsx). Fetch the necessary data (simulated here with an async function getData) and pass both the data and columns as props to the <DataTable /> component to render the complete table.

import { Payment, columns } 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>
  );
}

Enhancing Display: Cell Formatting

TanStack Table allows for custom rendering within cells. Let's format the amount column to display values as currency (USD) and right-align both the header and the cell content for better numerical readability.

Updating the Amount Column Definition

Modify the column definition for the amount key. We'll provide custom render functions for both the header and cell properties. The cell function receives the row context, allowing access to the row's data to perform formatting.

export const columns: ColumnDef<Payment>[] = [
  {
    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">{formatted}</div>;
    },
  },
];

You can use the same approach to format other cells and headers.

Adding Interactivity: Row Actions

Often, tables require actions that can be performed on individual rows (e.g., edit, delete, copy ID). We can add a dedicated column containing a trigger (like a button with an ellipsis icon) that opens a <DropdownMenu /> with relevant actions for that specific row.

Defining the Actions Column

Add a new column definition, typically with an id like "actions". This column won't have an accessorKey as it doesn't directly map to a data property. Instead, its cell render function will return the <DropdownMenu> component, configured with actions relevant to the row.original data.

"use client";

import { ColumnDef } 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: ColumnDef<Payment>[] = [
  // ...
  {
    id: "actions",
    cell: ({ row }) => {
      const payment = row.original;

      return (
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button variant="ghost" className="h-8 w-8 p-0">
              <span className="sr-only">Open menu</span>
              <MoreHorizontal className="h-4 w-4" />
            </Button>
          </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.

Handling Large Datasets: Pagination

For tables with many rows, pagination is essential for performance and usability. TanStack Table provides built-in pagination logic.

Enabling Pagination in <DataTable>

To enable pagination, import and provide the getPaginationRowModel function to the useReactTable hook options. This tells the table instance to manage pagination state.

import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  getPaginationRowModel,
  useReactTable,
} from "@tanstack/react-table";

export function DataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
  });

  // ...
}

This will automatically paginate your rows into pages of 10. See the pagination docs for more information on customizing page size and implementing manual pagination.

Implementing Pagination Controls

Render UI elements (like Buttons) to allow users to navigate between pages. Use the table instance's API methods (getCanPreviousPage, previousPage, getCanNextPage, nextPage) to control navigation and disable buttons when appropriate.

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

export function DataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
  })

  return (
    <div>
      <div className="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 section for a more advanced pagination component.

Enabling Data Sorting

Allow users to sort table data by clicking on column headers. TanStack Table handles the sorting state and logic.

Enabling Sorting in <DataTable>

Manage sorting state using React.useState. Import and provide getSortedRowModel to the useReactTable hook, along with the sorting state and the onSortingChange state setter.

"use client"

import * as React from "react"
import {
  ColumnDef,
  SortingState,
  flexRender,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from "@tanstack/react-table"

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

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    state: {
      sorting,
    },
  })

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

Making Header Cells Sortable

Modify the header render function for the desired column (e.g., email). Render a <Button> within the header. Use the column.toggleSorting method in the button's onClick handler to trigger sorting. You can also display an icon (like ArrowUpDown) to indicate sortability and current sort direction (column.getIsSorted()).

"use client";

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

export const columns: ColumnDef<Payment>[] = [
  {
    accessorKey: "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.

Implementing Data Filtering

Provide users with the ability to filter table rows based on input values. We'll add a simple text input to filter the email column.

Enabling Filtering in <DataTable>

Manage column filter state using React.useState. Import and provide getFilteredRowModel to the useReactTable hook, along with the columnFilters state and the onColumnFiltersChange state setter. Then, add an <Input> component outside the table structure. Use table.getColumn("columnId").setFilterValue() in the input's onChange handler to update the filter state for a specific column.

"use client"

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

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

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

  const table = useReactTable({
    data,
    columns,
    onSortingChange: setSorting,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    onColumnFiltersChange: setColumnFilters,
    getFilteredRowModel: getFilteredRowModel(),
    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="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 for more information on customizing filters.

Controlling Column Visibility

Allow users to dynamically show or hide table columns, which is useful for tables with many columns or for customizing the view.

Enabling Column Visibility in <DataTable>

Manage column visibility state using React.useState. Provide the onColumnVisibilityChange state setter to the useReactTable hook and include columnVisibility in the state option. Then, implement a UI control (like a <DropdownMenu> with <DropdownMenuCheckboxItem> components) that allows users to toggle the visibility of each column using column.toggleVisibility() and check the current state with column.getIsVisible(). Iterate over table.getAllColumns().filter(column => column.getCanHide()) to build the menu.

"use client"

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

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

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

  const table = useReactTable({
    data,
    columns,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    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 asChild>
            <Button variant="outline" className="ml-auto">
              Columns
            </Button>
          </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="rounded-md border">
        <Table>{ ... }</Table>
      </div>
    </div>
  )
}

This adds a dropdown menu that you can use to toggle column visibility.

Implementing Row Selection

Enable users to select one or multiple rows, often used for performing bulk actions.

Adding the Selection Column

Define a new column specifically for the selection checkboxes. This column typically has an id like "select". Its header render function will contain a "select all" checkbox tied to table.toggleAllPageRowsSelected(), and its cell render function will contain a checkbox for the individual row tied to row.toggleSelected(). Disable sorting and hiding for this column.

"use client";

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

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

export const columns: ColumnDef<Payment>[] = [
  {
    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,
  },
];

Enabling Row Selection in <DataTable>

Manage row selection state using React.useState. Provide the onRowSelectionChange state setter to the useReactTable hook and include rowSelection in the state option.

export function DataTable<TData, TValue>({
  columns,
  data,
}: 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,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    state: {
      sorting,
      columnFilters,
      columnVisibility,
      rowSelection,
    },
  });

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

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

Displaying Selection Count

Optionally, display feedback to the user about how many rows are currently selected. Access the selected rows using table.getFilteredSelectedRowModel().rows.length and display this count somewhere in the UI, often near the pagination controls.

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

Creating Reusable Table Components

As your data table features grow, you might identify patterns that can be extracted into reusable components to simplify your main DataTable implementation. The following examples are drawn from the Tasks demo application.

Reusable Column Header Component

Encapsulate the logic for sortable and hideable column headers into a dedicated component. This component would typically accept the column object and a title string as props.

export const columns = [
  {
    accessorKey: "email",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  },
];

Reusable Pagination Controls Component

Create a component that renders the complete set of pagination controls, including previous/next buttons, page number indicators, potentially a page size selector, and the selected row count display. This component would receive the table instance as a prop.

<DataTablePagination table={table} />

Reusable Column Visibility Toggle Component

Extract the dropdown menu used for toggling column visibility into its own component. This component would also receive the table instance as a prop.

<DataTableViewOptions table={table} />
    Data Table