Skip to content

Latest commit

 

History

History
790 lines (700 loc) · 19.4 KB

shadcn DataTable.md

File metadata and controls

790 lines (700 loc) · 19.4 KB

Shadcn Data Tables Docs

Data Table Powerful table and datagrids built using TanStack Table.

Docs Preview Code

Style: Default Copy Filter emails... Columns Status Email Amount

success [email protected] $316.00 Open menu

success [email protected] $242.00 Open menu

processing [email protected] $837.00 Open menu

success [email protected] $874.00 Open menu

failed [email protected] $721.00 Open menu 0 of 5 row(s) selected. PreviousNext 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 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

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 and the

component to build your own custom data table. We'll cover the following topics:

Basic Table Row Actions Pagination Sorting Filtering Visibility Row Selection Reusable Components Installation Add the

component to your project: npx shadcn@latest add table Copy Add tanstack/react-table dependency: npm install @tanstack/react-table Copy 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: "[email protected]", }, { id: "489e1d42", amount: 125, status: "processing", email: "[email protected]", }, // ... ] Copy Project Structure Start by creating the following file structure:

app └── payments ├── columns.tsx ├── data-table.tsx └── page.tsx Copy 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 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.

app/payments/columns.tsx "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[] = [ { accessorKey: "status", header: "Status", }, { accessorKey: "email", header: "Email", }, { accessorKey: "amount", header: "Amount", }, ] Copy 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.

component Next, we'll create a component to render our table.

app/payments/data-table.tsx "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 (

{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} ) })} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( <TableRow key={row.id} data-state={row.getIsSelected() && "selected"} > {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) ) : ( No results. )}
) } Copy Tip: If you find yourself using in multiple places, this is the component you could make reusable by extracting it to components/ui/data-table.tsx.

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

app/payments/page.tsx 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: "[email protected]", }, // ... ] }

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

return (

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

app/payments/columns.tsx export const columns: ColumnDef[] = [ { accessorKey: "amount", header: () =>

Amount
, 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>
},

}, ] Copy 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 component for this.

Update columns definition Update our columns definition to add a new actions column. The actions cell returns a component.

app/payments/columns.tsx "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[] = [ // ... { 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>
  )
},

}, // ... ] Copy 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 app/payments/data-table.tsx 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(), })

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

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

app/payments/data-table.tsx 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 (

{ // .... }
<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} > Previous
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} > Next ) } Copy See Reusable Components section for a more advanced pagination component.

Sorting Let's make the email column sortable.

Update app/payments/data-table.tsx "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([])

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

return (

{ ... }
) } Copy Make header cell sortable We can now update the email header cell to add sorting controls.

app/payments/columns.tsx "use client"

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

export const columns: ColumnDef[] = [ { accessorKey: "email", header: ({ column }) => { return ( <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} > Email ) }, }, ] Copy This will automatically sort the table (asc and desc) when the user toggles on the header cell.

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

Update app/payments/data-table.tsx "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([]) const [columnFilters, setColumnFilters] = React.useState( [] )

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

return (

<Input placeholder="Filter emails..." value={(table.getColumn("email")?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value) } className="max-w-sm" />
{ ... }
) } Copy 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.

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

Update app/payments/data-table.tsx "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([]) const [columnFilters, setColumnFilters] = React.useState( [] ) const [columnVisibility, setColumnVisibility] = React.useState({})

const table = useReactTable({ data, columns, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, state: { sorting, columnFilters, columnVisibility, }, })

return (

<Input placeholder="Filter emails..." value={table.getColumn("email")?.getFilterValue() as string} onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value) } className="max-w-sm" /> Columns {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} ) })}
{ ... }
) } Copy 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 app/payments/columns.tsx "use client"

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

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

export const columns: ColumnDef[] = [ { 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, }, ] Copy Update app/payments/data-table.tsx export function DataTable<TData, TValue>({ columns, data, }: DataTableProps<TData, TValue>) { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState( [] ) const [columnVisibility, setColumnVisibility] = React.useState({}) 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 (

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

{table.getFilteredSelectedRowModel().rows.length} of{" "} {table.getFilteredRowModel().rows.length} row(s) selected.
Copy Reusable Components Here are some components you can use to build your data tables. This is from the Tasks demo.

Column header Make any column header sortable and hideable.

Expand export const columns = [ { accessorKey: "email", header: ({ column }) => ( ), }, ] Copy Pagination Add pagination controls to your table including page size and selection count.

Expand Copy Column toggle A component to toggle column visibility.

Expand