Documentation
Datatable

Data Table

Powerful table and datagrids built using TanStack Table.

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 (opens in a new tab) 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.

💡

Tip: If you find yourself using the same table in multiple places in your app, you can always extract it into a reusable component.

Table of Contents

This guide will show you how to use TanStack Table (opens in a new tab) and the <Table /> component to build your own custom data table. We'll cover the following topics:

Installation

Add tanstack/react-table dependency:

npm install @tanstack/react-table

Prerequisites

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

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

Start by creating the following file structure:

      • columns.tsx
      • 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.tsx (client component) will contain our <DataTable /> component.
    • page.tsx (server component) is where we'll fetch data and render our table.

    Basic Table

    Let's start by building a basic table.

    Column Definitions

    First, we'll define our columns.

    "use client"
     
    import { ColumnDef } from "@tanstack/react-table"
     
    // 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",
      },
    ]
    💡

    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.

    <DataTable /> component

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

    "use client"
     
    import {
      Table,
      TableBody,
      TableCell,
      TableHead,
      TableHeader,
      TableRow,
    } from "@/components/ui/table"
    import {
      ColumnDef,
      flexRender,
      getCoreRowModel,
      useReactTable,
    } from "@tanstack/react-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>
      )
    }
    💡

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

    Render the table

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

    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>
      )
    }

    Cell Formatting

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

    Update columns definition

    Update the header and cell definitions for amount as follows:

    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.

    Row Actions

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

    Update columns definition

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

    "use client"
     
    import { Button } from "@/components/ui/button"
    import {
      DropdownMenu,
      DropdownMenuContent,
      DropdownMenuItem,
      DropdownMenuLabel,
      DropdownMenuSeparator,
      DropdownMenuTrigger,
    } from "@/components/ui/dropdown-menu"
    import { ColumnDef } from "@tanstack/react-table"
    import { MoreHorizontal } from "lucide-react"
     
    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="size-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.

    Pagination

    Next, we'll add pagination to our table.

    Update <DataTable>

    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 (opens in a new tab) for more information on customizing page size and implementing manual pagination.

    Add pagination controls

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

    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="secondary"
              size="sm"
              onClick={() => table.previousPage()}
              disabled={!table.getCanPreviousPage()}
            >
              Previous
            </Button>
            <Button
              variant="secondary"
              size="sm"
              onClick={() => table.nextPage()}
              disabled={!table.getCanNextPage()}
            >
              Next
            </Button>
          </div>
        </div>
      )
    }

    Server-side pagination

    If you're using a server-side data source, you'll need to implement server-side pagination. Here's how you can do that.

    Client component

    const router = useRouter()
    const pathname = usePathname()
    const searchParams = useSearchParams()
     
    // Search params
    const page = searchParams?.get("page") ?? "1"
    const per_page = searchParams?.get("per_page") ?? "10"
     
    // Create query string
    const createQueryString = React.useCallback(
      (params: Record<string, string | number | null>) => {
        const newSearchParams = new URLSearchParams(searchParams?.toString())
     
        for (const [key, value] of Object.entries(params)) {
          if (value === null) {
            newSearchParams.delete(key)
          } else {
            newSearchParams.set(key, String(value))
          }
        }
     
        return newSearchParams.toString()
      },
      [searchParams]
    )
     
    // Handle server-side pagination
    const [{ pageIndex, pageSize }, setPagination] =
      React.useState<PaginationState>({
        pageIndex: Number(page) - 1,
        pageSize: Number(per_page),
      })
     
    const pagination = React.useMemo(
      () => ({
        pageIndex,
        pageSize,
      }),
      [pageIndex, pageSize]
    )
     
    React.useEffect(() => {
      setPagination({
        pageIndex: Number(page) - 1,
        pageSize: Number(per_page),
      })
    }, [page, per_page])
     
    React.useEffect(() => {
      router.push(
        `${pathname}?${createQueryString({
          page: pageIndex + 1,
          per_page: pageSize,
        })}`,
        {
          // Don't scroll to top when paginating
          scroll: false,
        }
      )
     
    }, [pageIndex, pageSize])
     
    const table = useReactTable({
      data,
      columns,
      pageCount: pageCount ?? -1,
      state: {
        pagination, // Pass pagination to table state
      },
      onPaginationChange: setPagination, // Update pagination state when user paginates
      getCoreRowModel: getCoreRowModel(),
      getFilteredRowModel: getFilteredRowModel(),
      getPaginationRowModel: getPaginationRowModel(),
      getSortedRowModel: getSortedRowModel(),
      getFacetedRowModel: getFacetedRowModel(),
      getFacetedUniqueValues: getFacetedUniqueValues(),
      manualPagination: true, // Enable manual pagination
    })

    Server component

    interface PageProps {
      searchParams: {
        [key: string]: string | string[] | undefined
      }
    }
     
    export default async function Page({
      searchParams,
    }: ProductsPageProps) {
      const { page, per_page } = searchParams ?? {}
     
      // Number of items per page
      const limit = typeof per_page === "string" ? parseInt(per_page) : 10
      // Number of items to skip
      const offset =
        typeof page === "string"
          ? parseInt(page) > 0
            ? (parseInt(page) - 1) * limit
            : 0
          : 0
     
    // Use `limit` and `offset` to paginate your query
    // Here is an example using `Drizzle ORM`
    // Transaction is used to ensure both queries are executed in a single transaction
      const { items, count } = await db.transaction(async (tx) => {
        const items = await tx
          .select()
          .from(products)
          .limit(limit)
          .offset(offset)
     
        const count = await tx
          .select({
            count: sql<number>`count(${products.id})`,
          })
          .from(products)
          .execute()
          .then((res) => res[0]?.count ?? 0)
     
        return {
          items,
          count,
        }
      })
     
    // Calculate page count for pagination
      const pageCount = Math.ceil(count / limit)
     
    return (
       <DataTable
          data={data}
          pageCount={pageCount}
        />
    )

    See Reusable Components section for a more advanced pagination component.

    Sorting

    Let's make the email column sortable.

    Update <DataTable>

    "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>
      )
    }

    Make header cell sortable

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

    "use client"
     
    import { ColumnDef } from "@tanstack/react-table"
    import { ArrowUpDown, MoreHorizontal } 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 size-4" />
            </Button>
          )
        },
      },
    ]

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

    Server-side sorting

    If you're using a server-side data source, you'll need to implement server-side sorting. Here's how you can do that.

    Client component

    const router = useRouter()
    const pathname = usePathname()
    const searchParams = useSearchParams()
     
    // Search params
    const sort = searchParams?.get("sort")
    // Split sort param into column and order eg. `email.desc` => ["email", "desc"]
    const [column, order] = sort?.split(".") ?? []
     
    // Create query string
    const createQueryString = React.useCallback(
      (params: Record<string, string | number | null>) => {
        const newSearchParams = new URLSearchParams(searchParams?.toString())
     
        for (const [key, value] of Object.entries(params)) {
          if (value === null) {
            newSearchParams.delete(key)
          } else {
            newSearchParams.set(key, String(value))
          }
        }
     
        return newSearchParams.toString()
      },
      [searchParams]
    )
     
    // Handle server-side sorting
    const [sorting, setSorting] = React.useState<SortingState>([
      {
        id: column ?? "",
        desc: order === "desc",
      },
    ])
     
    React.useEffect(() => {
      router.push(
        `${pathname}?${createQueryString({
          page,
          sort: sorting[0]?.id
            ? `${sorting[0]?.id}.${sorting[0]?.desc ? "desc" : "asc"}`
            : null,
        })}`,
        {
          scroll: false,
        }
      )
     
    }, [sorting])
     
    const table = useReactTable({
      data,
      columns,
      state: {
        sorting, // Pass sorting to table state
      },
      onSortingChange: setSorting, // Update sorting state when user toggles sorting
      getCoreRowModel: getCoreRowModel(),
      getFilteredRowModel: getFilteredRowModel(),
      getPaginationRowModel: getPaginationRowModel(),
      getSortedRowModel: getSortedRowModel(),
      getFacetedRowModel: getFacetedRowModel(),
      getFacetedUniqueValues: getFacetedUniqueValues(),
      manualSorting: true, // Enable manual sorting
    })

    Server component

    interface PageProps {
      searchParams: {
        [key: string]: string | string[] | undefined
      }
    }
     
    export default async function Page({
      searchParams,
    }: ProductsPageProps) {
      const { sort } = searchParams ?? {}
     
      // Column and order to sort by
      const [column, order] = typeof sort === "string" ? (sort.split(".")) : []
     
    // Use `column` and `order` to sort your query
    // Here is an example using `Drizzle ORM`
    // Transaction is used to ensure both queries are executed in a single transaction
      const { items, count } = await db.transaction(async (tx) => {
        const items = await tx
          .select()
          .from(products)
          .orderBy(
            column && column in products
              ? order === "asc"
                ? asc(products[column])
                : desc(products[column])
              : desc(products.createdAt)
          )
     
        const count = await tx
          .select({
            count: sql<number>`count(${products.id})`,
          })
          .from(products)
          .execute()
          .then((res) => res[0]?.count ?? 0)
     
        return {
          items,
          count,
        }
      })
     
    return (
       <DataTable
          data={data}
        />
    )

    Filtering

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

    Update <DataTable>

    "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 (opens in a new tab) for more information on customizing filters.

    Server-side filtering

    If you're using a server-side data source, you'll need to implement server-side filtering. Here's how you can do that.

    Client component

    const router = useRouter()
    const pathname = usePathname()
    const searchParams = useSearchParams()
     
    // Search params
    const sort = searchParams?.get("sort")
    // Split sort param into column and order eg. `email.desc` => ["email", "desc"]
    const [column, order] = sort?.split(".") ?? []
     
    // Create query string
    const createQueryString = React.useCallback(
      (params: Record<string, string | number | null>) => {
        const newSearchParams = new URLSearchParams(searchParams?.toString())
     
        for (const [key, value] of Object.entries(params)) {
          if (value === null) {
            newSearchParams.delete(key)
          } else {
            newSearchParams.set(key, String(value))
          }
        }
     
        return newSearchParams.toString()
      },
      [searchParams]
    )
     
    // Handle server-side sorting
    const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
    // Using `useDebounce` hook to debounce email filter
    const debouncedEmail = useDebounce(
      columnFilters.find((filter) => filter.id === "email")?.value,
      500
    )
     
    React.useEffect(() => {
      router.push(
        `${pathname}?${createQueryString({
          page: 1,
          email: typeof debouncedEmail === "string" ? debouncedEmail : null,
        })}`,
        {
          scroll: false,
        }
      )
    }, [debouncedEmail])
     
    const table = useReactTable({
      data,
      columns,
      state: {
        columnFilters, // Pass column filters to table state
      },
      onColumnFiltersChange: setColumnFilters, // Update column filters state when user filters
      getCoreRowModel: getCoreRowModel(),
      getFilteredRowModel: getFilteredRowModel(),
      getPaginationRowModel: getPaginationRowModel(),
      getSortedRowModel: getSortedRowModel(),
      getFacetedRowModel: getFacetedRowModel(),
      getFacetedUniqueValues: getFacetedUniqueValues(),
      manualFiltering: true, // Enable manual filtering
    })

    Server component

    interface PageProps {
      searchParams: {
        [key: string]: string | string[] | undefined
      }
    }
     
    export default async function Page({
      searchParams,
    }: ProductsPageProps) {
      const { email } = searchParams ?? {}
     
      // Column and order to sort by
      const [column, order] = typeof sort === "string" ? (sort.split(".")) : []
     
    // Use `column` and `order` to sort your query
    // Here is an example using `Drizzle ORM`
    // Transaction is used to ensure both queries are executed in a single transaction
      const { items, count } = await db.transaction(async (tx) => {
        const items = await tx
          .select()
          .from(orders)
          .where(
            and(
              // Filter by email
              typeof email === "string"
                ? like(orders.email, `%${email}%`)
                : undefined,
              // Other filters
            )
          )
     
        const count = await tx
          .select({
            count: sql<number>`count(*)`,
          })
          .from(orders)
          .execute()
          .then((res) => res[0]?.count ?? 0)
     
        return {
          items,
          count,
        }
      })
     
    return (
       <DataTable
          data={data}
        />
    )

    We can use the data-table-faceted-filter component to add faceted filters to our table. See Reusable Components for more information.

    Visibility

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

    Update <DataTable>

    "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="secondary" 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.

    Row Selection

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

    Update column definitions

    "use client"
     
    import { Badge } from "@/components/ui/badge"
    import { Checkbox } from "@/components/ui/checkbox"
    import { ColumnDef } from "@tanstack/react-table"
     
    export const columns: ColumnDef<Payment>[] = [
      {
        id: "select",
        header: ({ table }) => (
          <Checkbox
            checked={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>

    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.

    Show selected rows

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

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

    Reusable Components

    Column header

    Make any column header sortable and hideable.

    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 space-x-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" ? (
                  <ArrowDown className="ml-2 size-4" />
                ) : column.getIsSorted() === "asc" ? (
                  <ArrowUp className="ml-2 size-4" />
                ) : (
                  <ChevronsUpDown className="ml-2 size-4" />
                )}
              </Button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="start">
              <DropdownMenuItem onClick={() => column.toggleSorting(false)}>
                <ArrowUp className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
                Asc
              </DropdownMenuItem>
              <DropdownMenuItem onClick={() => column.toggleSorting(true)}>
                <ArrowDown className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
                Desc
              </DropdownMenuItem>
              <DropdownMenuSeparator />
              <DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
                <EyeOff className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
                Hide
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </div>
      )
    }

    Example

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

    Pagination

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

    interface DataTablePaginationProps<TData> {
      table: Table<TData>
    }
     
    export function DataTablePagination<TData>({
      table,
    }: DataTablePaginationProps<TData>) {
      return (
        <div className="flex items-center justify-between px-2">
          <div className="text-muted-foreground flex-1 text-sm">
            {table.getFilteredSelectedRowModel().rows.length} of{" "}
            {table.getFilteredRowModel().rows.length} row(s) selected.
          </div>
          <div className="flex items-center space-x-6 lg:space-x-8">
            <div className="flex items-center space-x-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">
                  {[10, 20, 30, 40, 50].map((pageSize) => (
                    <SelectItem key={pageSize} value={`${pageSize}`}>
                      {pageSize}
                    </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 space-x-2">
              <Button
                variant="secondary"
                className="hidden h-8 w-8 p-0 lg:flex"
                onClick={() => table.setPageIndex(0)}
                disabled={!table.getCanPreviousPage()}
              >
                <span className="sr-only">Go to first page</span>
                <DoubleArrowLeftIcon className="size-4" />
              </Button>
              <Button
                variant="secondary"
                className="h-8 w-8 p-0"
                onClick={() => table.previousPage()}
                disabled={!table.getCanPreviousPage()}
              >
                <span className="sr-only">Go to previous page</span>
                <ChevronLeftIcon className="size-4" />
              </Button>
              <Button
                variant="secondary"
                className="h-8 w-8 p-0"
                onClick={() => table.nextPage()}
                disabled={!table.getCanNextPage()}
              >
                <span className="sr-only">Go to next page</span>
                <ChevronRightIcon className="size-4" />
              </Button>
              <Button
                variant="secondary"
                className="hidden h-8 w-8 p-0 lg:flex"
                onClick={() => table.setPageIndex(table.getPageCount() - 1)}
                disabled={!table.getCanNextPage()}
              >
                <span className="sr-only">Go to last page</span>
                <DoubleArrowRightIcon className="size-4" />
              </Button>
            </div>
          </div>
        </div>
      )
    }

    Advanced pagination

    Advanced pagination component with page size, page count, and go to page, and page navigation.

    interface DataTablePaginationProps<TData>
      extends React.HTMLAttributes<HTMLDivElement> {
      table: Table<TData>
      pageSizeOptions?: number[]
    }
     
    export function DataTablePagination<TData>({
      table,
      pageSizeOptions = [10, 20, 30, 40, 50],
      className,
      ...props
    }: DataTablePaginationProps<TData>) {
      return (
        <div
          className={cn(
            "flex w-full flex-col items-center justify-between gap-4 overflow-auto px-2 py-1 sm:flex-row sm:gap-8",
            className
          )}
          {...props}
        >
          <div className="text-muted-foreground flex-1 whitespace-nowrap text-sm">
            {table.getFilteredSelectedRowModel().rows.length} of{" "}
            {table.getFilteredRowModel().rows.length} row(s) selected.
          </div>
          <div className="flex flex-col items-center gap-4 sm:flex-row">
            <div className="flex items-center space-x-2">
              <p className="whitespace-nowrap text-sm font-medium">Rows</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((pageSize) => (
                    <SelectItem key={pageSize} value={pageSize}>
                      {pageSize}
                    </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 space-x-2 whitespace-nowrap">
              <div>Go to</div>
              <Input
                type="number"
                min={1}
                max={table.getPageCount()}
                defaultValue={table.getState().pagination.pageIndex + 1}
                onChange={(e) => {
                  const page = e.target.value ? Number(e.target.value) - 1 : 0
                  table.setPageIndex(page)
                }}
                className="h-8 w-16"
              />
            </div>
            <div className="flex items-center space-x-2">
              <Button
                aria-label="Go to first page"
                variant="secondary"
                size="icon"
                className="hidden h-8 w-8 lg:flex"
                onClick={() => table.setPageIndex(0)}
                disabled={!table.getCanPreviousPage()}
              >
                <ChevronsLeft className="size-4" aria-hidden="true" />
              </Button>
              <Button
                aria-label="Go to previous page"
                variant="secondary"
                size="icon"
                className="h-8 w-8"
                onClick={() => table.previousPage()}
                disabled={!table.getCanPreviousPage()}
              >
                <ChevronLeft className="size-4" aria-hidden="true" />
              </Button>
              <Button
                aria-label="Go to next page"
                variant="secondary"
                size="icon"
                className="h-8 w-8"
                onClick={() => table.nextPage()}
                disabled={!table.getCanNextPage()}
              >
                <ChevronRight className="size-4" aria-hidden="true" />
              </Button>
              <Button
                aria-label="Go to last page"
                variant="secondary"
                size="icon"
                className="hidden h-8 w-8 lg:flex"
                onClick={() => table.setPageIndex(table.getPageCount() - 1)}
                disabled={!table.getCanNextPage()}
              >
                <ChevronsRight className="size-4" aria-hidden="true" />
              </Button>
            </div>
          </div>
        </div>
      )
    }

    Example

    Column toggle

    A component to toggle column visibility.

    interface DataTableViewOptionsProps<TData> {
      table: Table<TData>
    }
     
    export function DataTableViewOptions<TData>({
      table,
    }: DataTableViewOptionsProps<TData>) {
      return (
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button
              variant="secondary"
              size="sm"
              className="ml-auto hidden h-8 lg:flex"
            >
              <MixerHorizontalIcon className="mr-2 size-4" />
              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) => {
                return (
                  <DropdownMenuCheckboxItem
                    key={column.id}
                    className="capitalize"
                    checked={column.getIsVisible()}
                    onCheckedChange={(value) => column.toggleVisibility(!!value)}
                  >
                    {column.id}
                  </DropdownMenuCheckboxItem>
                )
              })}
          </DropdownMenuContent>
        </DropdownMenu>
      )
    }

    Faceted filter

    A component to filter your table columns by facets.

    interface DataTableFacetedFilterProps<TData, TValue> {
      column?: Column<TData, TValue>
      title?: string
      options: {
        label: string
        value: string
        icon?: React.ComponentType<{ className?: string }>
      }[]
    }
     
    export function DataTableFacetedFilter<TData, TValue>({
      column,
      title,
      options,
    }: DataTableFacetedFilter<TData, TValue>) {
      const selectedValues = new Set(column?.getFilterValue() as string[])
     
      return (
        <Popover>
          <PopoverTrigger asChild>
            <Button
              aria-label="Filter rows"
              variant="secondary"
              className="border-dashed"
            >
              <PlusCircle className="mr-2 size-4" aria-hidden="true" />
              {title}
              {selectedValues?.size > 0 && (
                <>
                  <Separator orientation="vertical" className="mx-2 h-4" />
                  <Badge
                    variant="secondary"
                    className="rounded-sm px-1 font-normal lg:hidden"
                  >
                    {selectedValues.size}
                  </Badge>
                  <div className="hidden space-x-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(
                            "border-primary mr-2 flex size-4 items-center justify-center rounded-sm border",
                            isSelected
                              ? "bg-primary text-primary-foreground"
                              : "opacity-50 [&_svg]:invisible"
                          )}
                        >
                          <Check className={cn("size-4")} aria-hidden="true" />
                        </div>
                        {option.icon && (
                          <option.icon
                            className="text-muted-foreground mr-2 size-4"
                            aria-hidden="true"
                          />
                        )}
                        <span>{option.label}</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>
      )
    }

    Example

    Advanced filter

    A component to filter your table columns by notion-like filters.

    interface DataTableAdvancedFilterProps<TData> {
      filterableColumns?: DataTableFilterableColumn<TData>[]
      searchableColumns?: DataTableSearchableColumn<TData>[]
      selectedOptions: DataTableFilterOption<TData>[]
      setSelectedOptions: React.Dispatch<
        React.SetStateAction<DataTableFilterOption<TData>[]>
      >
      filterOpen: boolean
      setFilterOpen: React.Dispatch<React.SetStateAction<boolean>>
      isSwitchable?: boolean
    }
     
    export function DataTableAdvancedFilter<TData>({
      filterableColumns = [],
      searchableColumns = [],
      selectedOptions,
      setSelectedOptions,
      filterOpen,
      setFilterOpen,
      isSwitchable = false,
    }: DataTableAdvancedFilterProps<TData>) {
      const [value, setValue] = React.useState("")
      const [open, setOpen] = React.useState(false)
     
      const options: DataTableFilterOption<TData>[] = React.useMemo(() => {
        const searchableOptions = searchableColumns.map((column) => ({
          label: String(column.id),
          value: column.id,
          items: [],
        }))
        const filterableOptions = filterableColumns.map((column) => ({
          label: column.title,
          value: column.id,
          items: column.options,
        }))
        return [...searchableOptions, ...filterableOptions]
      }, [searchableColumns, filterableColumns])
     
      React.useEffect(() => {
        if (selectedOptions.length === 0) {
          setFilterOpen(false)
          setValue("")
        }
      }, [selectedOptions, setFilterOpen])
     
      return (
        <>
          {isSwitchable ? (
            <Button variant="secondary" onClick={() => setFilterOpen(!filterOpen)}>
              Filter
              <ChevronsUpDown
                className="ml-2 size-4 opacity-50"
                aria-hidden="true"
              />
            </Button>
          ) : (
            <Popover open={open} onOpenChange={setOpen}>
              <PopoverTrigger asChild>
                {filterOpen ? (
                  options.filter(
                    (option) =>
                      !selectedOptions.find((item) => item.value === option.value)
                  ).length > 0 ? (
                    <Button
                      variant="secondary"
                      size="sm"
                      role="combobox"
                      className="h-8 rounded-full"
                    >
                      <Plus className="mr-2 size-4 opacity-50" aria-hidden="true" />
                      Add filter
                    </Button>
                  ) : null
                ) : (
                  <Button variant="secondary" role="combobox">
                    Filter
                    <ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
                  </Button>
                )}
              </PopoverTrigger>
              <PopoverContent className="w-[200px] p-0" align="end">
                <Command>
                  <CommandInput placeholder="Filter by..." />
                  <CommandEmpty>No item found.</CommandEmpty>
                  <CommandGroup>
                    {options
                      .filter(
                        (option) =>
                          !selectedOptions.find(
                            (item) => item.value === option.value
                          )
                      )
                      .map((option) => (
                        <CommandItem
                          key={String(option.value)}
                          className="capitalize"
                          onSelect={(currentValue) => {
                            setValue(currentValue === value ? "" : currentValue)
                            setOpen(false)
                            setFilterOpen(
                              selectedOptions.length > 0 ? true : !filterOpen
                            )
                            setSelectedOptions?.((prev) => {
                              if (currentValue === value) {
                                return prev.filter(
                                  (item) => item.value !== option.value
                                )
                              } else {
                                return [...prev, option]
                              }
                            })
                          }}
                        >
                          {option.label}
                          <Check
                            className={cn(
                              "ml-auto size-4",
                              value === option.value ? "opacity-100" : "opacity-0"
                            )}
                            aria-hidden="true"
                          />
                        </CommandItem>
                      ))}
                  </CommandGroup>
                </Command>
              </PopoverContent>
            </Popover>
          )}
        </>
      )
    }
     
    interface DataTableAdvancedFilterItemProps<TData> {
      table: Table<TData>
      selectedOption: DataTableFilterOption<TData>
      setSelectedOptions: React.Dispatch<
        React.SetStateAction<DataTableFilterOption<TData>[]>
      >
    }
     
    export function DataTableAdvancedFilterItem<TData>({
      table,
      selectedOption,
      setSelectedOptions,
    }: DataTableAdvancedFilterItemProps<TData>) {
      const [value, setValue] = React.useState("")
      const [open, setOpen] = React.useState(true)
     
      const selectedValues =
        selectedOption.items.length > 0
          ? Array.from(
              new Set(
                table
                  .getColumn(String(selectedOption.value))
                  ?.getFilterValue() as string[]
              )
            )
          : []
     
      const filterVarieties =
        selectedOption.items.length > 0
          ? ["is", "is not"]
          : ["contains", "does not contain", "is", "is not"]
     
      const [filterVariety, setFilterVariety] = React.useState(filterVarieties[0])
     
      return (
        <Popover open={open} onOpenChange={setOpen}>
          <PopoverTrigger asChild>
            <Button
              variant="secondary"
              className={cn(
                "h-7 truncate rounded-full",
                (selectedValues.length > 0 || value.length > 0) && "bg-muted/50"
              )}
            >
              {value.length > 0 || selectedValues.length > 0 ? (
                <>
                  <span className="font-medium capitalize">
                    {selectedOption.label}:
                  </span>
                  {selectedValues.length > 0 ? (
                    <span className="ml-1">
                      {selectedValues.length > 2
                        ? selectedValues.length + " selected"
                        : selectedValues.join(", ")}
                    </span>
                  ) : (
                    <span className="ml-1">{value}</span>
                  )}
                </>
              ) : (
                <span className="capitalize">{selectedOption.label}</span>
              )}
            </Button>
          </PopoverTrigger>
          <PopoverContent className="w-60 space-y-1 text-xs" align="start">
            <div className="flex items-center space-x-1">
              <div className="flex flex-1 items-center space-x-1">
                <div className="capitalize">{selectedOption.label}</div>
                <Select onValueChange={(value) => setFilterVariety(value)}>
                  <SelectTrigger className="hover:bg-muted/50 h-auto w-fit truncate bg-transparent px-2 py-0.5">
                    <SelectValue placeholder={filterVarieties[0]} />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectGroup>
                      {filterVarieties.map((variety) => (
                        <SelectItem key={variety} value={variety}>
                          {variety}
                        </SelectItem>
                      ))}
                    </SelectGroup>
                  </SelectContent>
                </Select>
              </div>
              <Button
                aria-label="Remove filter"
                variant="ghost"
                size="icon"
                className="h-8 w-8"
                onClick={() => {
                  table
                    .getColumn(String(selectedOption.value))
                    ?.setFilterValue(undefined)
     
                  setSelectedOptions((prev) =>
                    prev.filter((item) => item.value !== selectedOption.value)
                  )
                }}
              >
                <Trash className="size-4" aria-hidden />
              </Button>
            </div>
            {selectedOption.items.length > 0 ? (
              table.getColumn(
                selectedOption.value ? String(selectedOption.value) : ""
              ) && (
                <DataTableFacetedFilter
                  key={String(selectedOption.value)}
                  column={table.getColumn(
                    selectedOption.value ? String(selectedOption.value) : ""
                  )}
                  title={selectedOption.label}
                  options={selectedOption.items}
                  variant="command"
                />
              )
            ) : (
              <Input
                placeholder="Type here..."
                className="h-8"
                value={value}
                onChange={(event) => {
                  setValue(event.target.value)
                  table
                    .getColumn(String(selectedOption.value))
                    ?.setFilterValue(event.target.value)
                }}
                autoFocus
              />
            )}
          </PopoverContent>
        </Popover>
      )
    }
     
    interface DataTableFacetedFilter<TData, TValue> {
      column?: Column<TData, TValue>
      title?: string
      options: Option[]
      variant?: "popover" | "command"
    }
     
    export function DataTableFacetedFilter<TData, TValue>({
      column,
      title,
      options,
      variant = "popover",
    }: DataTableFacetedFilter<TData, TValue>) {
      const selectedValues = new Set(column?.getFilterValue() as string[])
     
      return (
        <>
          {variant === "popover" ? (
            <Popover>
              <PopoverTrigger asChild>
                <Button variant="secondary" size="sm" className="h-8 border-dashed">
                  <PlusCircle className="mr-2 size-4" />
                  {title}
                  {selectedValues?.size > 0 && (
                    <>
                      <Separator orientation="vertical" className="mx-2 h-4" />
                      <Badge
                        variant="secondary"
                        className="rounded-sm px-1 font-normal lg:hidden"
                      >
                        {selectedValues.size}
                      </Badge>
                      <div className="hidden space-x-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(
                                "border-primary mr-2 flex size-4 items-center justify-center rounded-sm border",
                                isSelected
                                  ? "bg-primary text-primary-foreground"
                                  : "opacity-50 [&_svg]:invisible"
                              )}
                            >
                              <Check className={cn("size-4")} aria-hidden="true" />
                            </div>
                            {option.icon && (
                              <option.icon
                                className="text-muted-foreground mr-2 size-4"
                                aria-hidden="true"
                              />
                            )}
                            <span>{option.label}</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>
          ) : (
            <Command className="p-1">
              <CommandInput
                placeholder={title}
                autoFocus
                showIcon={false}
                className="border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-8 w-full rounded-md border bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50"
              />
              <CommandList className="mt-1">
                <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(
                            "border-primary mr-2 flex size-4 items-center justify-center rounded-sm border",
                            isSelected
                              ? "bg-primary text-primary-foreground"
                              : "opacity-50 [&_svg]:invisible"
                          )}
                        >
                          <Check className={cn("size-4")} aria-hidden="true" />
                        </div>
                        {option.icon && (
                          <option.icon
                            className="text-muted-foreground mr-2 size-4"
                            aria-hidden="true"
                          />
                        )}
                        <span>{option.label}</span>
                      </CommandItem>
                    )
                  })}
                </CommandGroup>
                {selectedValues.size > 0 && (
                  <>
                    <CommandSeparator />
                    <CommandGroup>
                      <CommandItem
                        onSelect={() => column?.setFilterValue(undefined)}
                        className="justify-center text-center"
                      >
                        Clear filters
                      </CommandItem>
                    </CommandGroup>
                  </>
                )}
              </CommandList>
            </Command>
          )}
        </>
      )
    }

    Example

    Complex example

    Notion like filters with search and facets (opens in a new tab)

    Toolbar

    A component to show a toolbar with search, and filters. It is built with the DataTableFacetedFilter and DataTableAdvancedFilter components.

    interface DataTableToolbarProps<TData> {
      table: Table<TData>
      searchableColumns?: DataTableSearchableColumn<TData>[]
      filterableColumns?: DataTableFilterableColumn<TData>[]
      advancedFilter?: boolean
    }
     
    export function DataTableToolbar<TData>({
      table,
      filterableColumns = [],
      searchableColumns = [],
      advancedFilter = false,
    }: DataTableToolbarProps<TData>) {
      const isFiltered = table.getState().columnFilters.length > 0
      const [selectedOptions, setSelectedOptions] = React.useState<
        DataTableFilterOption<TData>[]
      >([])
      const [advancedFilterMenuOpen, setAdvancedFilterMenuOpen] =
        React.useState(false)
     
      return (
        <div className="w-full space-y-2.5 overflow-auto p-1">
          <div className="flex items-center justify-between space-x-2">
            <div className="flex flex-1 items-center space-x-2">
              {searchableColumns.length > 0 &&
                searchableColumns.map(
                  (column) =>
                    table.getColumn(column.id ? String(column.id) : "") && (
                      <Input
                        key={String(column.id)}
                        placeholder={`Filter ${column.title}...`}
                        value={
                          (table
                            .getColumn(String(column.id))
                            ?.getFilterValue() as string) ?? ""
                        }
                        onChange={(event) =>
                          table
                            .getColumn(String(column.id))
                            ?.setFilterValue(event.target.value)
                        }
                        className="h-8 w-[150px] lg:w-[250px]"
                      />
                    )
                )}
              {!advancedFilter &&
                filterableColumns.length > 0 &&
                filterableColumns.map(
                  (column) =>
                    table.getColumn(column.id ? String(column.id) : "") && (
                      <DataTableFacetedFilter
                        key={String(column.id)}
                        column={table.getColumn(column.id ? String(column.id) : "")}
                        title={column.title}
                        options={column.options}
                      />
                    )
                )}
              {!advancedFilter && isFiltered && (
                <Button
                  variant="ghost"
                  onClick={() => table.resetColumnFilters()}
                  className="h-8 px-2 lg:px-3"
                >
                  Reset
                  <Cross2Icon className="ml-2 size-4" />
                </Button>
              )}
            </div>
            <div className="flex items-center space-x-2">
              {advancedFilter ? (
                <DataTableAdvancedFilter
                  searchableColumns={searchableColumns}
                  filterableColumns={filterableColumns}
                  selectedOptions={selectedOptions}
                  setSelectedOptions={setSelectedOptions}
                  advancedFilterMenuOpen={advancedFilterMenuOpen}
                  setAdvancedFilterMenuOpen={setAdvancedFilterMenuOpen}
                  isSwitchable={selectedOptions.length > 0}
                />
              ) : null}
              <DataTableViewOptions table={table} />
            </div>
          </div>
          {advancedFilter && advancedFilterMenuOpen ? (
            <div className="flex items-center space-x-2">
              {selectedOptions.map((selectedOption) => (
                <DataTableAdvancedFilterItem
                  key={String(selectedOption.value)}
                  table={table}
                  selectedOption={selectedOption}
                  setSelectedOptions={setSelectedOptions}
                  advancedFilterMenuOpen={advancedFilterMenuOpen}
                  setAdvancedFilterMenuOpen={setAdvancedFilterMenuOpen}
                />
              ))}
              <DataTableAdvancedFilter
                searchableColumns={searchableColumns}
                filterableColumns={filterableColumns}
                selectedOptions={selectedOptions}
                setSelectedOptions={setSelectedOptions}
                advancedFilterMenuOpen={advancedFilterMenuOpen}
                setAdvancedFilterMenuOpen={setAdvancedFilterMenuOpen}
                isSwitchable={false}
              />
            </div>
          ) : null}
        </div>
      )
    }

    Floating bar

    A component to show a floating bar to manage bulk actions when table rows are selected.

    interface DataTableFloatingBarProps<TData>
      extends React.HTMLAttributes<HTMLElement> {
      table: Table<TData>
    }
     
    export function DataTableFloatingBar<TData>({
      table,
      className,
      ...props
    }: DataTableFloatingBarProps<TData>) {
      if (table.getFilteredSelectedRowModel().rows.length <= 0) return null
     
      function updateTasksStatus(table: Table<TData>, status: string) {
        const selectedRows = table.getFilteredSelectedRowModel()
          .rows as unknown as { original: Task }[]
     
        selectedRows.map(async (row) => {
          await updateTaskStatusAction({
            id: row.original.id,
            status: status as Task["status"],
          })
        })
      }
     
      function updateTasksPriority(table: Table<TData>, priority: string) {
        const selectedRows = table.getFilteredSelectedRowModel()
          .rows as unknown as { original: Task }[]
     
        selectedRows.map(async (row) => {
          await updateTaskPriorityAction({
            id: row.original.id,
            priority: priority as Task["priority"],
          })
        })
      }
     
      return (
        <div
          className={cn(
            "mx-auto flex w-fit items-center gap-2 rounded-md bg-zinc-900 px-4 py-2 text-xs text-white",
            className
          )}
          {...props}
        >
          <Button
            aria-label="Clear selection"
            className="h-auto bg-transparent p-1 text-white hover:bg-zinc-700"
            onClick={() => table.toggleAllRowsSelected(false)}
          >
            <Cross2Icon className="size-4" aria-hidden="true" />
          </Button>
          {table.getFilteredSelectedRowModel().rows.length} row(s) selected
          <Select onValueChange={(value) => updateTasksStatus(table, value)}>
            <SelectTrigger asChild>
              <Button className="h-auto bg-transparent p-1 text-white hover:bg-zinc-700">
                <CheckCircledIcon className="size-4" aria-hidden="true" />
              </Button>
            </SelectTrigger>
            <SelectContent align="center">
              <SelectGroup>
                {tasks.status.enumValues.map((status) => (
                  <SelectItem key={status} value={status} className="capitalize">
                    {status}
                  </SelectItem>
                ))}
              </SelectGroup>
            </SelectContent>
          </Select>
          <Select onValueChange={(value) => updateTasksPriority(table, value)}>
            <SelectTrigger asChild>
              <Button className="h-auto bg-transparent p-1 text-white hover:bg-zinc-700">
                <ArrowUpIcon className="size-4" aria-hidden="true" />
              </Button>
            </SelectTrigger>
            <SelectContent align="center">
              <SelectGroup>
                {tasks.priority.enumValues.map((priority) => (
                  <SelectItem
                    key={priority}
                    value={priority}
                    className="capitalize"
                  >
                    {priority}
                  </SelectItem>
                ))}
              </SelectGroup>
            </SelectContent>
          </Select>
          <Button
            aria-label="Delete selected rows"
            className="h-auto bg-transparent p-1 text-white hover:bg-zinc-700"
            onClick={() => {
              // table.getFilteredSelectedRowModel().rows.map((row) => row.id)
            }}
          >
            <TrashIcon className="size-4" aria-hidden="true" />
          </Button>
        </div>
      )
    }

    Example

    Loading skeleton

    A component to show a loading skeleton for your table.

    interface DataTableLoadingProps {
      columnCount: number
      rowCount?: number
      searchableFieldCount?: number
      filterableFieldCount?: number
    }
     
    export function DataTableLoading({
      columnCount,
      rowCount = 10,
      searchableFieldCount = 1,
      filterableFieldCount = 1,
    }: DataTableLoadingProps) {
      return (
        <div className="w-full space-y-3 overflow-auto">
          <div className="flex w-full items-center justify-between space-x-2 overflow-auto p-1">
            <div className="flex flex-1 items-center space-x-2">
              {searchableFieldCount > 0
                ? Array.from({ length: searchableFieldCount }).map((_, i) => (
                    <Skeleton key={i} className="h-7 w-[150px] lg:w-[250px]" />
                  ))
                : null}
              {filterableFieldCount > 0
                ? Array.from({ length: filterableFieldCount }).map((_, i) => (
                    <Skeleton key={i} className="h-7 w-[70px] border-dashed" />
                  ))
                : null}
            </div>
          </div>
          <div className="rounded-md border">
            <Table className="min-w-[640px]">
              <TableHeader>
                {Array.from({ length: 1 }).map((_, i) => (
                  <TableRow key={i} className="hover:bg-transparent">
                    {Array.from({ length: columnCount }).map((_, i) => (
                      <TableHead key={i}>
                        <Skeleton className="h-6 w-full" />
                      </TableHead>
                    ))}
                  </TableRow>
                ))}
              </TableHeader>
              <TableBody>
                {Array.from({ length: rowCount }).map((_, i) => (
                  <TableRow key={i} className="hover:bg-transparent">
                    {Array.from({ length: columnCount }).map((_, j) => (
                      <TableCell key={j}>
                        <Skeleton className="h-6 w-full" />
                      </TableCell>
                    ))}
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          </div>
          <div className="flex w-full flex-col items-center justify-between gap-4 overflow-auto px-2 py-1 sm:flex-row sm:gap-8">
            <div className="flex-1">
              <Skeleton className="h-8 w-40" />
            </div>
            <div className="flex flex-col items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8">
              <div className="flex items-center space-x-2">
                <Skeleton className="h-8 w-24" />
                <Skeleton className="h-8 w-[70px]" />
              </div>
              <div className="flex w-[100px] items-center justify-center text-sm font-medium">
                <Skeleton className="h-8 w-20" />
              </div>
              <div className="flex items-center space-x-2">
                <Skeleton className="hidden h-8 w-8 lg:block" />
                <Skeleton className="h-8 w-8" />
                <Skeleton className="h-8 w-8" />
                <Skeleton className="hidden h-8 w-8 lg:block" />
              </div>
            </div>
          </div>
        </div>
      )
    }

    Example

    Dnd-kit

    You can use dnd-kit (opens in a new tab) to add drag and drop support to your table.

    Install dependencies

    npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/modifiers @dnd-kit/utilities

    Setup dnd-kit in our component

    We need to add the following:

    • Store in a state the table data that will allow, through the setter, to update the table elements when they start to move.
    • Store in a state the active id that is in movement.
    • Sensors of dnd-kit for the correct operation of the drag, touch and keyboard with dnd through the useSensors hook.
    • Callbacks needed to handle dnd in its different states: start, end and cancel.
    • A memoized value for the selected row
    ...
    const [tableData, setTableData] = React.useState<Payment[]>(data);
     
    const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);
    const items = React.useMemo(() => tableData?.map(({ id }) => id), [tableData]);
     
    const sensors = useSensors(
      useSensor(MouseSensor, {}),
      useSensor(TouchSensor, {}),
      useSensor(KeyboardSensor, {})
    );
     
    const handleDragStart = React.useCallback(({ active: { id: activeId } }: DragStartEvent) => {
      setActiveId(activeId);
    }, []);
     
    const handleDragEnd = React.useCallback((event: DragEndEvent) => {
      const { active, over } = event;
      if (over && active.id !== over.id) {
        setTableData((data) => {
          const oldIndex = items.indexOf(active.id as string);
          const newIndex = items.indexOf(over?.id as string);
          return arrayMove(data, oldIndex, newIndex);
        });
      }
      setActiveId(null);
    }, [items]);
     
    const handleDragCancel = React.useCallback(() => {
      setActiveId(null);
    }, []);
    ...

    Add DndContext with Callbacks

    ...
    return (
      <DndContext
        sensors={sensors}
        onDragEnd={handleDragEnd}
        onDragStart={handleDragStart}
        onDragCancel={handleDragCancel}
        collisionDetection={closestCenter}
        modifiers={[restrictToVerticalAxis]}
      >
        ...
      </DndContext>
    )
    ...

    Add SortableContext to the table body

    ...
    <TableBody>
      <SortableContext items={items} strategy={verticalListSortingStrategy}>
        {table.getRowModel().rows?.length ? (
          table.getRowModel().rows.map((row) => (
            <DraggableTableRow key={row.id} row={row} />
          ))
        ) : (
          <TableRow>
            <TableCell
              colSpan={columns.length}
              className="h-24 text-center"
            >
              No results.
            </TableCell>
          </TableRow>
        )}
      </SortableContext>
    </TableBody>
    ...

    Add DragOverlay (if required)

    ...
    <DragOverlay>
      {activeId && (
        <Table>
          <TableBody>
            <TableRow>
              {selectedRow?.getVisibleCells().map((cell) => {
                return (
                  <TableCell key={cell.id}>
                    {flexRender(
                      cell.column.columnDef.cell,
                      cell.getContext()
                    )}
                  </TableCell>
                )
              })}
            </TableRow>
          </TableBody>
        </Table>
      )}
    </DragOverlay>
    ...

    Add DraggableTableRow component

    const DraggableTableRow = ({ row }: { row: Row<Payment> }) => {
      const {
        attributes,
        listeners,
        transform,
        transition,
        setNodeRef,
        isDragging,
      } = useSortable({
        id: row.original.id,
      })
     
      const style = {
        transform: CSS.Transform.toString(transform),
        transition,
      }
     
      return (
        <TableRow
          ref={setNodeRef}
          style={style}
          key={row.id}
          data-state={row.getIsSelected() && "selected"}
        >
          {isDragging ? (
            <TableCell
              colSpan={row.getAllCells().length}
              className="h-16 w-full bg-slate-300/40"
            >
              &nbsp;
            </TableCell>
          ) : (
            row.getVisibleCells().map((cell) => {
              if (cell.id.includes("drag")) {
                return (
                  <TableCell
                    key={cell.id}
                    {...attributes}
                    {...listeners}
                    className={cn(isDragging ? "cursor-grabbing" : "cursor-grab")}
                  >
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </TableCell>
                )
              }
              return (
                <TableCell key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </TableCell>
              )
            })
          )}
        </TableRow>
      )
    }

    Done

    The complete example is here: