Dependencies
tailwindcssshadcn/ui@tanstack/react-tablelucide-react
shadcn/ui components
npx shadcn@latest add tablenpx shadcn@latest add buttonnpx shadcn@latest add inputnpx shadcn@latest add dropdown-menunpx shadcn@latest add badge

TanStack Table v8 + shadcn/ui Table is the recommended pattern for data tables in modern React apps. This page covers 3 patterns — basic table, sortable/filterable and paginated.

Installation

npm install @tanstack/react-table
npx shadcn@latest add table button input dropdown-menu

Core Concepts

TanStack Table is headless — it gives you state and behavior, you provide the UI:

const table = useReactTable({
  data,                              // your data array
  columns,                           // column definitions
  getCoreRowModel: getCoreRowModel(), // required
  getSortedRowModel: getSortedRowModel(),   // for sorting
  getFilteredRowModel: getFilteredRowModel(), // for search
  getPaginationRowModel: getPaginationRowModel(), // for pagination
})

Then render with shadcn Table components and flexRender.

1. Basic shadcn/ui Table

Static data table using shadcn/ui Table components — no library needed for simple tables.

tsx
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"

const templates = [
  { id: 1, name: "Marvel Angular Dashboard", framework: "Angular 21", price: "$29", sales: 1240, status: "Active" },
  { id: 2, name: "PORTO Bootstrap Template", framework: "Bootstrap 5", price: "$19", sales: 3580, status: "Active" },
  { id: 3, name: "Proctu Medical React", framework: "React 19", price: "$39", sales: 890, status: "Draft" },
  { id: 4, name: "Kiosk Next.js Dashboard", framework: "Next.js 15", price: "$29", sales: 620, status: "Active" },
  { id: 5, name: "CANVA Bootstrap UI Kit", framework: "Bootstrap 5", price: "$15", sales: 2100, status: "Active" },
]

const statusColor: Record<string, string> = {
  Active: "bg-green-100 text-green-700",
  Draft: "bg-yellow-100 text-yellow-700",
  Archived: "bg-gray-100 text-gray-600",
}

export default function BasicTable() {
  return (
    <div className="rounded-xl border bg-white overflow-hidden">
      <Table>
        <TableHeader>
          <TableRow className="bg-gray-50">
            <TableHead className="font-semibold">Template</TableHead>
            <TableHead className="font-semibold">Framework</TableHead>
            <TableHead className="font-semibold">Price</TableHead>
            <TableHead className="font-semibold">Sales</TableHead>
            <TableHead className="font-semibold">Status</TableHead>
            <TableHead className="font-semibold text-right">Actions</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {templates.map((row) => (
            <TableRow key={row.id} className="hover:bg-gray-50">
              <TableCell className="font-medium">{row.name}</TableCell>
              <TableCell className="text-muted-foreground text-sm">{row.framework}</TableCell>
              <TableCell className="font-semibold" style={{ color: "#fd4766" }}>{row.price}</TableCell>
              <TableCell>{row.sales.toLocaleString()}</TableCell>
              <TableCell>
                <span className={`px-2 py-0.5 rounded-full text-xs font-medium ${statusColor[row.status]}`}>
                  {row.status}
                </span>
              </TableCell>
              <TableCell className="text-right">
                <div className="flex justify-end gap-1">
                  <Button variant="ghost" size="sm">Edit</Button>
                  <Button variant="ghost" size="sm" className="text-destructive hover:text-destructive">Delete</Button>
                </div>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}

2. Sortable & Filterable Table with TanStack

Full-featured data table using TanStack Table v8 with column sorting and search filter.

tsx
import { useState, useMemo } from "react"
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  flexRender,
  type ColumnDef,
  type SortingState,
} from "@tanstack/react-table"
import {
  Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from "@/components/ui/table"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { ArrowUpDown, ArrowUp, ArrowDown, Search } from "lucide-react"

type Template = {
  name: string
  framework: string
  price: number
  sales: number
  status: string
}

const data: Template[] = [
  { name: "Marvel Angular Dashboard", framework: "Angular", price: 29, sales: 1240, status: "Active" },
  { name: "PORTO Bootstrap Template", framework: "Bootstrap", price: 19, sales: 3580, status: "Active" },
  { name: "Proctu Medical React", framework: "React", price: 39, sales: 890, status: "Draft" },
  { name: "Kiosk Next.js Dashboard", framework: "Next.js", price: 29, sales: 620, status: "Active" },
  { name: "CANVA UI Kit", framework: "Bootstrap", price: 15, sales: 2100, status: "Active" },
]

const SortIcon = ({ sorted }: { sorted: false | "asc" | "desc" }) => {
  if (!sorted) return <ArrowUpDown size={14} className="text-muted-foreground opacity-50" />
  return sorted === "asc"
    ? <ArrowUp size={14} style={{ color: "#fd4766" }} />
    : <ArrowDown size={14} style={{ color: "#fd4766" }} />
}

export default function SortableTable() {
  const [sorting, setSorting] = useState<SortingState>([])
  const [globalFilter, setGlobalFilter] = useState("")

  const columns = useMemo<ColumnDef<Template>[]>(() => [
    {
      accessorKey: "name",
      header: ({ column }) => (
        <button className="flex items-center gap-1.5 font-semibold hover:text-foreground"
          onClick={() => column.toggleSorting()}>
          Template <SortIcon sorted={column.getIsSorted()} />
        </button>
      ),
    },
    {
      accessorKey: "framework",
      header: ({ column }) => (
        <button className="flex items-center gap-1.5 font-semibold hover:text-foreground"
          onClick={() => column.toggleSorting()}>
          Framework <SortIcon sorted={column.getIsSorted()} />
        </button>
      ),
    },
    {
      accessorKey: "price",
      header: ({ column }) => (
        <button className="flex items-center gap-1.5 font-semibold hover:text-foreground"
          onClick={() => column.toggleSorting()}>
          Price <SortIcon sorted={column.getIsSorted()} />
        </button>
      ),
      cell: ({ row }) => (
        <span className="font-semibold" style={{ color: "#fd4766" }}>
          ${row.original.price}
        </span>
      ),
    },
    {
      accessorKey: "sales",
      header: ({ column }) => (
        <button className="flex items-center gap-1.5 font-semibold hover:text-foreground"
          onClick={() => column.toggleSorting()}>
          Sales <SortIcon sorted={column.getIsSorted()} />
        </button>
      ),
      cell: ({ row }) => row.original.sales.toLocaleString(),
    },
    {
      accessorKey: "status",
      header: "Status",
      cell: ({ row }) => (
        <span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
          row.original.status === "Active"
            ? "bg-green-100 text-green-700"
            : "bg-yellow-100 text-yellow-700"
        }`}>
          {row.original.status}
        </span>
      ),
    },
  ], [])

  const table = useReactTable({
    data,
    columns,
    state: { sorting, globalFilter },
    onSortingChange: setSorting,
    onGlobalFilterChange: setGlobalFilter,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
  })

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-2">
        <div className="relative flex-1 max-w-xs">
          <Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
          <Input
            placeholder="Search templates..."
            value={globalFilter}
            onChange={(e) => setGlobalFilter(e.target.value)}
            className="pl-9"
          />
        </div>
        <span className="text-sm text-muted-foreground">
          {table.getFilteredRowModel().rows.length} results
        </span>
      </div>

      <div className="rounded-xl border bg-white overflow-hidden">
        <Table>
          <TableHeader>
            {table.getHeaderGroups().map((hg) => (
              <TableRow key={hg.id} className="bg-gray-50 hover:bg-gray-50">
                {hg.headers.map((header) => (
                  <TableHead key={header.id}>
                    {flexRender(header.column.columnDef.header, header.getContext())}
                  </TableHead>
                ))}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {table.getRowModel().rows.length === 0 ? (
              <TableRow>
                <TableCell colSpan={columns.length} className="text-center py-8 text-muted-foreground">
                  No results found
                </TableCell>
              </TableRow>
            ) : (
              table.getRowModel().rows.map((row) => (
                <TableRow key={row.id} className="hover:bg-gray-50">
                  {row.getVisibleCells().map((cell) => (
                    <TableCell key={cell.id}>
                      {flexRender(cell.column.columnDef.cell, cell.getContext())}
                    </TableCell>
                  ))}
                </TableRow>
              ))
            )}
          </TableBody>
        </Table>
      </div>
    </div>
  )
}

Frequently Asked Questions

Run npm install @tanstack/react-table. It's framework-agnostic — you provide the data and column definitions, TanStack handles sorting, filtering, pagination and selection logic. No UI opinions included — you render the table yourself using shadcn/ui Table components.
TanStack Table is headless — no UI, just logic. You build the UI yourself with shadcn/ui or any component library. AG Grid is a full-featured grid with its own UI, virtual scrolling, Excel export and enterprise features. For most dashboards TanStack Table is sufficient and much lighter.
Enable row selection: useReactTable({ enableRowSelection: true }). Add a checkbox column using the 'select' accessor. Access selected rows with table.getSelectedRowModel().rows. Row selection state can be managed externally with useState.
Set manualPagination: true and pageCount from your API response. On pagination state change fetch new data from the server passing the current page and pageSize. Update the data passed to useReactTable when the fetch completes.

Need a Full React + Next.js Dashboard Template?

Get a complete React + Next.js dashboard with 50+ components — built by the same team behind BootstrapPlanet.

Browse Templates →

Use code FIRST30 for 30% off.

Related Components