# Data Table

> Search, filter, sort, select, and act on rows of application data.

Source: https://www.honestui.com/docs/product/data-table

```tsx
"use client"

import {
  DataTable,
  type DataTableProps,
} from "@/registry/default/product/data-table/data-table"

type Customer = {
  id: string
  name: string
  email: string
  status: "Active" | "Pending" | "Inactive"
  plan: string
  spent: number
}

const customers: Customer[] = [
  { id: "1", name: "Olivia Rhye", email: "olivia@acmeforge.com", status: "Active", plan: "Pro", spent: 1234 },
  { id: "2", name: "Phoenix Baker", email: "phoenix@northwind.io", status: "Active", plan: "Pro", spent: 2342 },
  { id: "3", name: "Liam Carter", email: "liam@globex.dev", status: "Pending", plan: "Starter", spent: 0 },
  { id: "4", name: "Sofia Reyes", email: "sofia@initech.co", status: "Inactive", plan: "Starter", spent: 480 },
  { id: "5", name: "Ethan Brooks", email: "ethan@umbrella.ai", status: "Active", plan: "Business", spent: 8210 },
  { id: "6", name: "Maya Patel", email: "maya@hooli.net", status: "Pending", plan: "Pro", spent: 990 },
  { id: "7", name: "Noah Thompson", email: "noah@vandelay.com", status: "Active", plan: "Starter", spent: 120 },
  { id: "8", name: "Isla Campbell", email: "isla@duffbrewing.com", status: "Inactive", plan: "Business", spent: 4120 },
  { id: "9", name: "Lucas Martin", email: "lucas@craytek.io", status: "Active", plan: "Pro", spent: 2675 },
  { id: "10", name: "Amelia Davis", email: "amelia@sterling.co", status: "Pending", plan: "Starter", spent: 45 },
  { id: "11", name: "Henry Wilson", email: "henry@massive.dev", status: "Active", plan: "Business", spent: 9870 },
  { id: "12", name: "Zara Ahmed", email: "zara@pixelworks.io", status: "Active", plan: "Pro", spent: 3310 },
]

const columns: DataTableProps<Customer>["columns"] = [
  { accessorKey: "name", header: "Customer" },
  { accessorKey: "email", header: "Email" },
  { accessorKey: "status", header: "Status" },
  { accessorKey: "plan", header: "Plan" },
  {
    accessorKey: "spent",
    header: "Spent",
    meta: { align: "right", label: "Spent" },
    cell: ({ row }) =>
      new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        maximumFractionDigits: 0,
      }).format(row.original.spent),
  },
]

export function DataTableDemo() {
  return (
    <div className="w-full min-w-0 max-w-4xl">
      <DataTable
        columns={columns}
        data={customers}
        caption="Customers"
        getRowId={(row) => row.id}
        pagination
      />
    </div>
  )
}

```

## Overview [#overview]

Use Data Table for records that people need to find and act on. Invoice queues and customer directories often need search, status filters, row selection, and pagination. Data Table adds those controls to [Table](/docs/components/table).

Use Table when you only need to present rows and columns. Add Data Table when users need to narrow the records, change which columns they see, or select rows for an action.

## Anatomy [#anatomy]

```text
DataTable
├── Toolbar: search, filters, columns, actions
├── Content: header row, data rows, states
└── Footer: selection summary, pagination
```

`DataTable` renders all three regions. Data Table also exports each part separately: `DataTableToolbar`, `DataTableSearch`, `DataTableFilter`, `DataTableViewOptions`, `DataTableContent`, `DataTableFooter`, `DataTableSelectionSummary`, and `DataTablePagination`. Use the separate parts to change their order or add controls.

The rows render in a native HTML table with `<thead>`, `<th scope="col">`, and `<tbody>` elements. Column headers stay visible during vertical scrolling. Wide tables scroll horizontally instead of turning each row into a card.

## Behavior [#behavior]

Data Table handles sorting, filters, column visibility, selection, and pagination in the browser by default. To handle one of these on your server, pass its controlled state and callback. Set `manualSorting`, `manualFiltering`, or `manualPagination` for the matching task.

Keep each task in one place. If the server paginates 10,000 records, it must also sort them. Sorting the ten rows in the browser only sorts the current page and gives the wrong result.

Search updates on every change unless you set `debounceMs`. A filter can include several values from the same column. The Columns menu lists columns that TanStack Table allows users to hide. When users select rows, the footer reports the count and provides a Clear action.

## Accessibility [#accessibility]

Use `caption` to name the table. Screen readers announce this name before the cells. Sortable headers render as buttons with names such as "Sort by Customer." The `<th>` element reports the current direction through `aria-sort`.

Use `getRowLabel` to name each selection checkbox. Return a phrase such as "Select Olivia Rhye" instead of a row number. The pagination range uses `aria-live`. Page controls report their state through `aria-current` and `aria-disabled`.

Do not use color as the only status cue. A green status badge should also say "Paid."

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;data-table&#x22;]" />
  

  
    
      
        Install the required dependency:
      

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

      
        Copy and paste the following code into your project.
      

      <ComponentSource name="data-table" title="components/ui/data-table/data-table.tsx" file="data-table.tsx" />

      <ComponentSource name="data-table" title="components/ui/data-table/data-table-column-header.tsx" file="data-table-column-header.tsx" />

      <ComponentSource name="data-table" title="components/ui/data-table/data-table-context.tsx" file="data-table-context.tsx" />

      <ComponentSource name="data-table" title="components/ui/data-table/data-table-filter.tsx" file="data-table-filter.tsx" />

      <ComponentSource name="data-table" title="components/ui/data-table/data-table-pagination.tsx" file="data-table-pagination.tsx" />

      <ComponentSource name="data-table" title="components/ui/data-table/data-table-toolbar.tsx" file="data-table-toolbar.tsx" />

      <ComponentSource name="data-table" title="components/ui/data-table/data-table-view-options.tsx" file="data-table-view-options.tsx" />

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

Pass the columns and rows. Then turn on search, selection, or pagination as needed:

```tsx
import { DataTable } from "@/components/ui/data-table/data-table"

const columns = [
  { accessorKey: "name", header: "Customer" },
  { accessorKey: "email", header: "Email" },
  {
    accessorKey: "spent",
    header: "Spent",
    meta: { align: "right" },
  },
]

<DataTable
  columns={columns}
  data={customers}
  caption="Customers"
  search={{ placeholder: "Search customers..." }}
  selectable
  pagination={{ pageSizeOptions: [10, 20, 50] }}
/>
```

Columns use TanStack Table's `ColumnDef` format. A cell can contain any React content, including a link, badge, menu, or formatted number. Set `meta.align: "right"` on numeric columns so the digits line up.

### Server-side mode [#server-side-mode]

When rows come from an API, keep search, sorting, filters, and pagination state in your application. Pass that state to Data Table and use each callback to request new rows. Set the matching `manual*` flags and pass `rowCount`. Pass `pageCount` too if the server already calculates it.

This example handles search, sorting, and pagination on the server:

```tsx
<DataTable
  columns={columns}
  data={query.data.rows}
  rowCount={query.data.total}
  loading={query.isFetching}
  search={{ placeholder: "Search orders...", debounceMs: 250 }}
  manualSorting
  manualFiltering
  manualPagination
  sorting={sorting}
  onSortingChange={setSorting}
  globalFilter={globalFilter}
  onGlobalFilterChange={setGlobalFilter}
  pagination={pagination}
  onPaginationChange={setPagination}
/>
```

## Don't do this [#dont-do-this]

### Treating no results as no data [#treating-no-results-as-no-data]

```tsx
// Bad. This hides the recovery path.
{rows.length === 0 ? <p>Nothing here yet</p> : null}
```

```tsx
// Good. Each state has its own message.
<DataTable
  columns={columns}
  data={rows}
  emptyState={<EmptyAddCustomer />}
  // No-results state offers Clear filters automatically
/>
```

An empty dataset has no records. A no-results state still has records, but the current search or filters exclude them. Use separate messages so users know whether to add a record or clear the current view.

## Examples [#examples]

### Search and filters [#search-and-filters]

Search across the table or filter by status. The Filters button reports how many filters are active and includes a Clear all action. Choose filters that exclude every row to see the no-results state.

```tsx
"use client"

import {
  DataTable,
  type DataTableProps,
} from "@/registry/default/product/data-table/data-table"
import { Badge } from "@/components/honest-ui/ui/badge"

type Invoice = {
  id: string
  number: string
  customer: string
  status: "Paid" | "Open" | "Overdue" | "Draft"
  amount: number
}

const invoices: Invoice[] = [
  { id: "1", number: "INV-1042", customer: "Acme Forge", status: "Paid", amount: 2400 },
  { id: "2", number: "INV-1043", customer: "Northwind Labs", status: "Open", amount: 1180 },
  { id: "3", number: "INV-1044", customer: "Globex Studio", status: "Overdue", amount: 760 },
  { id: "4", number: "INV-1045", customer: "Initech Co", status: "Paid", amount: 3250 },
  { id: "5", number: "INV-1046", customer: "Umbrella AI", status: "Draft", amount: 940 },
  { id: "6", number: "INV-1047", customer: "Hooli Networks", status: "Open", amount: 1520 },
  { id: "7", number: "INV-1048", customer: "Vandelay Imports", status: "Paid", amount: 640 },
  { id: "8", number: "INV-1049", customer: "Duff Brewing", status: "Overdue", amount: 2810 },
]

const statusVariant = {
  Paid: "success",
  Open: "info",
  Overdue: "error",
  Draft: "neutral",
} as const

const columns: DataTableProps<Invoice>["columns"] = [
  { accessorKey: "number", header: "Invoice" },
  { accessorKey: "customer", header: "Customer" },
  {
    accessorKey: "status",
    header: "Status",
    cell: ({ row }) => (
      <Badge variant={statusVariant[row.original.status]}>
        {row.original.status}
      </Badge>
    ),
  },
  {
    accessorKey: "amount",
    header: "Amount",
    meta: { align: "right" },
    cell: ({ row }) =>
      new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        maximumFractionDigits: 0,
      }).format(row.original.amount),
  },
]

export function DataTableSearchFilters() {
  return (
    <div className="w-full min-w-0 max-w-4xl">
      <DataTable
        columns={columns}
        data={invoices}
        caption="Invoices"
        getRowId={(row) => row.id}
        search={{ placeholder: "Search invoices..." }}
        filters={[
          {
            columnId: "status",
            title: "Status",
            options: [
              { label: "Paid", value: "Paid" },
              { label: "Open", value: "Open" },
              { label: "Overdue", value: "Overdue" },
              { label: "Draft", value: "Draft" },
            ],
          },
        ]}
        pagination
      />
    </div>
  )
}

```

### Selection and bulk actions [#selection-and-bulk-actions]

Select one row or all rows on the page. The footer reports the selection count and clears it. The example connects selected rows to bulk actions and gives each row an actions menu.

```tsx
"use client"

import * as React from "react"
import {
  Copy as CopyIcon,
  Download as DownloadIcon,
  Ellipsis as EllipsisIcon,
  Mail as MailIcon,
  Pencil as PencilIcon,
  Trash as TrashIcon,
} from "honestui/icons"

import {
  DataTable,
  DataTableContent,
  DataTableFooter,
  DataTableSelectionSummary,
  type DataTableProps,
} from "@/registry/default/product/data-table/data-table"
import { Button } from "@/components/honest-ui/ui/button"
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuSeparator,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"

type Member = {
  id: string
  name: string
  role: string
  team: string
}

const members: Member[] = [
  { id: "1", name: "Olivia Rhye", role: "Owner", team: "Platform" },
  { id: "2", name: "Phoenix Baker", role: "Admin", team: "Platform" },
  { id: "3", name: "Liam Carter", role: "Developer", team: "Mobile" },
  { id: "4", name: "Sofia Reyes", role: "Developer", team: "Web" },
  { id: "5", name: "Ethan Brooks", role: "Developer", team: "Web" },
  { id: "6", name: "Maya Patel", role: "Viewer", team: "Design" },
  { id: "7", name: "Noah Thompson", role: "Admin", team: "Design" },
  { id: "8", name: "Isla Campbell", role: "Developer", team: "Mobile" },
]

function RowActions({ member }: { member: Member }) {
  return (
    <Menu>
      <MenuTrigger
        render={<Button variant="ghost" size="icon-sm" aria-label={`More actions for ${member.name}`} />}
      >
        <EllipsisIcon />
      </MenuTrigger>
      <MenuPopup align="end">
        <MenuItem>
          <PencilIcon className="opacity-72" />
          Edit member
        </MenuItem>
        <MenuItem>
          <CopyIcon className="opacity-72" />
          Duplicate
        </MenuItem>
        <MenuSeparator />
        <MenuItem variant="destructive">
          <TrashIcon className="opacity-72" />
          Remove
        </MenuItem>
      </MenuPopup>
    </Menu>
  )
}

function BulkActions() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="outline" size="sm" />}>
        Bulk actions
      </MenuTrigger>
      <MenuPopup align="start">
        <MenuItem>
          <DownloadIcon className="opacity-72" />
          Export selected
        </MenuItem>
        <MenuItem>
          <MailIcon className="opacity-72" />
          Send invite
        </MenuItem>
        <MenuSeparator />
        <MenuItem variant="destructive">
          <TrashIcon className="opacity-72" />
          Remove
        </MenuItem>
      </MenuPopup>
    </Menu>
  )
}

const columns: DataTableProps<Member>["columns"] = [
  { accessorKey: "name", header: "Name" },
  { accessorKey: "role", header: "Role" },
  { accessorKey: "team", header: "Team" },
  {
    id: "actions",
    enableSorting: false,
    enableHiding: false,
    meta: { align: "right" },
    header: "",
    cell: ({ row }) => <RowActions member={row.original} />,
  },
]

export function DataTableSelection() {
  const [selected, setSelected] = React.useState<Record<string, boolean>>({})

  return (
    <div className="w-full min-w-0 max-w-4xl">
      <DataTable
        columns={columns}
        data={members}
        caption="Team members"
        getRowId={(row) => row.id}
        getRowLabel={(row) => `Select ${row.original.name}`}
        selectable
        rowSelection={selected}
        onRowSelectionChange={setSelected}
      >
        <DataTableContent />
        <DataTableFooter className="flex-wrap gap-x-[var(--hui-space-4)] gap-y-[var(--hui-space-2)] px-[var(--hui-space-4)] py-[var(--hui-space-3)]">
          <DataTableSelectionSummary actions={<BulkActions />} />
        </DataTableFooter>
      </DataTable>
    </div>
  )
}

```

### Custom cells [#custom-cells]

Render React content inside cells. This example uses avatars, status badges, right-aligned currency, and formatted dates.

```tsx
"use client"

import {
  DataTable,
  type DataTableProps,
} from "@/registry/default/product/data-table/data-table"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/honest-ui/ui/avatar"
import { Badge } from "@/components/honest-ui/ui/badge"

type Deal = {
  id: string
  name: string
  username: string
  avatar: string
  initials: string
  status: "Won" | "In progress" | "Lost"
  value: number
  closeDate: Date
}

const deals: Deal[] = [
  { id: "1", name: "Olivia Rhye", username: "@olivia", avatar: "https://i.pravatar.cc/64?img=1", initials: "OR", status: "Won", value: 12400, closeDate: new Date("2026-01-12T00:00:00Z") },
  { id: "2", name: "Phoenix Baker", username: "@phoenix", avatar: "https://i.pravatar.cc/64?img=2", initials: "PB", status: "In progress", value: 8200, closeDate: new Date("2026-02-03T00:00:00Z") },
  { id: "3", name: "Liam Carter", username: "@liam", avatar: "https://i.pravatar.cc/64?img=3", initials: "LC", status: "Lost", value: 3100, closeDate: new Date("2026-02-18T00:00:00Z") },
  { id: "4", name: "Sofia Reyes", username: "@sofia", avatar: "https://i.pravatar.cc/64?img=4", initials: "SR", status: "In progress", value: 15750, closeDate: new Date("2026-03-02T00:00:00Z") },
  { id: "5", name: "Ethan Brooks", username: "@ethan", avatar: "https://i.pravatar.cc/64?img=5", initials: "EB", status: "Won", value: 4300, closeDate: new Date("2026-03-21T00:00:00Z") },
]

const statusVariant = {
  Won: "success",
  "In progress": "info",
  Lost: "neutral",
} as const

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
})

const date = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "2-digit",
  year: "numeric",
  timeZone: "UTC",
})

const columns: DataTableProps<Deal>["columns"] = [
  {
    accessorKey: "name",
    header: "Deal owner",
    cell: ({ row }) => (
      <div className="flex items-center gap-[var(--hui-space-3)]">
        <Avatar size="4">
          <AvatarImage src={row.original.avatar} alt="" />
          <AvatarFallback>{row.original.initials}</AvatarFallback>
        </Avatar>
        <span>
          <span className="block text-[var(--hui-color-foreground-base-primary)]">
            {row.original.name}
          </span>
          <span className="block text-[var(--hui-color-foreground-base-secondary)] [font-size:var(--hui-font-size-micro)]">
            {row.original.username}
          </span>
        </span>
      </div>
    ),
  },
  {
    accessorKey: "status",
    header: "Status",
    cell: ({ row }) => (
      <Badge variant={statusVariant[row.original.status]}>
        {row.original.status}
      </Badge>
    ),
  },
  {
    accessorKey: "value",
    header: "Value",
    meta: { align: "right" },
    cell: ({ row }) => (
      <span className="tracking-[-0.02em]">
        {currency.format(row.original.value)}
      </span>
    ),
  },
  {
    accessorKey: "closeDate",
    header: "Close date",
    sortingFn: "datetime",
    cell: ({ row }) => (
      <span className="tracking-[-0.02em] [word-spacing:-0.06em]">
        {date.format(row.original.closeDate)}
      </span>
    ),
  },
]

export function DataTableCustomCells() {
  return (
    <div className="w-full min-w-0 max-w-4xl">
      <DataTable
        columns={columns}
        data={deals}
        caption="Deals"
        getRowId={(row) => row.id}
        pagination
      />
    </div>
  )
}

```

### Loading [#loading]

Skeleton rows replace the table body while data loads. The toolbar, header, and footer stay in place.

```tsx
"use client"

import * as React from "react"

import {
  DataTable,
  type DataTableProps,
} from "@/registry/default/product/data-table/data-table"

type Deployment = {
  id: string
  commit: string
  environment: string
  status: "Ready" | "Building"
}

const deployments: Deployment[] = [
  { id: "1", commit: "a1f2e3c", environment: "Production", status: "Ready" },
  { id: "2", commit: "b4c5d6e", environment: "Staging", status: "Ready" },
  { id: "3", commit: "f7a8b9c", environment: "Preview", status: "Building" },
  { id: "4", commit: "0d1e2f3", environment: "Production", status: "Ready" },
  { id: "5", commit: "4a5b6c7", environment: "Preview", status: "Ready" },
  { id: "6", commit: "8d9e0f1", environment: "Staging", status: "Building" },
  { id: "7", commit: "2a3b4c5", environment: "Production", status: "Ready" },
]

const columns: DataTableProps<Deployment>["columns"] = [
  { accessorKey: "commit", header: "Commit" },
  { accessorKey: "environment", header: "Environment" },
  { accessorKey: "status", header: "Status" },
]

export function DataTableLoading() {
  const [loading, setLoading] = React.useState(true)

  React.useEffect(() => {
    const timeout = setTimeout(() => setLoading(false), 1500)

    return () => clearTimeout(timeout)
  }, [])

  return (
    <div className="w-full min-w-0 max-w-4xl">
      <DataTable
        columns={columns}
        data={deployments}
        caption="Deployments"
        getRowId={(row) => row.id}
        loading={loading}
        pagination
      />
    </div>
  )
}

```

### Error [#error]

When a request fails, the table shows the error and a Try again action instead of a no-results message.

```tsx
"use client"

import * as React from "react"

import {
  DataTable,
  type DataTableProps,
} from "@/registry/default/product/data-table/data-table"

type Project = {
  id: string
  name: string
  language: string
  visibility: "Public" | "Private"
}

const projects: Project[] = [
  { id: "1", name: "atlas-api", language: "TypeScript", visibility: "Private" },
  { id: "2", name: "atlas-web", language: "TypeScript", visibility: "Public" },
  { id: "3", name: "design-tokens", language: "CSS", visibility: "Public" },
  { id: "4", name: "edge-functions", language: "Rust", visibility: "Private" },
  { id: "5", name: "docs-engine", language: "Go", visibility: "Public" },
]

const columns: DataTableProps<Project>["columns"] = [
  { accessorKey: "name", header: "Project" },
  { accessorKey: "language", header: "Language" },
  { accessorKey: "visibility", header: "Visibility" },
]

export function DataTableError() {
  const [state, setState] = React.useState<"error" | "loading" | "ready">(
    "error",
  )

  const retry = () => {
    setState("loading")
    setTimeout(() => setState("ready"), 1200)
  }

  return (
    <div className="w-full min-w-0 max-w-4xl">
      <DataTable
        columns={columns}
        data={projects}
        caption="Projects"
        getRowId={(row) => row.id}
        error={
          state === "error" ? "Could not load projects." : null
        }
        loading={state === "loading"}
        onRetry={retry}
        pagination
      />
    </div>
  )
}

```

### Empty data [#empty-data]

Use `emptyState` when the dataset has no records. Its message should explain how to add the first one.

```tsx
"use client"

import {
  DataTable,
  type DataTableProps,
} from "@/registry/default/product/data-table/data-table"
import { Inbox as InboxIcon } from "honestui/icons"
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/honest-ui/ui/empty"

type ApiKey = {
  id: string
  name: string
  scope: string
}

const columns: DataTableProps<ApiKey>["columns"] = [
  { accessorKey: "name", header: "Name" },
  { accessorKey: "scope", header: "Scope" },
]

export function DataTableEmpty() {
  return (
    <div className="w-full min-w-0 max-w-4xl">
      <DataTable
        columns={columns}
        data={[]}
        caption="API keys"
        emptyState={
          <Empty className="p-[var(--hui-space-8)]">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <InboxIcon />
              </EmptyMedia>
              <EmptyTitle>No API keys yet</EmptyTitle>
              <EmptyDescription>
                Keys you create will appear here with their scope and last used
                date.
              </EmptyDescription>
            </EmptyHeader>
          </Empty>
        }
      />
    </div>
  )
}

```

### Controlled server table [#controlled-server-table]

This example sends search, sorting, filters, and pagination changes to the server. It debounces search input and shows a loading state during each request.

```tsx
"use client"

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

import {
  DataTable,
  type DataTableProps,
} from "@/registry/default/product/data-table/data-table"

type Order = {
  id: string
  number: string
  customer: string
  status: "Fulfilled" | "Processing" | "Cancelled"
  total: number
}

const ALL_ORDERS: Order[] = Array.from({ length: 37 }, (_, index) => {
  const statuses = ["Fulfilled", "Processing", "Cancelled"] as const
  const customers = [
    "Acme Forge",
    "Northwind Labs",
    "Globex Studio",
    "Initech Co",
    "Umbrella AI",
    "Hooli Networks",
  ]

  return {
    id: String(index + 1),
    number: `SO-${4100 + index}`,
    customer: customers[index % customers.length],
    status: statuses[index % statuses.length],
    total: ((index * 137) % 900) + 60,
  }
})

const PAGE_SIZE = 10

const columns: DataTableProps<Order>["columns"] = [
  { accessorKey: "number", header: "Order" },
  { accessorKey: "customer", header: "Customer" },
  { accessorKey: "status", header: "Status" },
  {
    accessorKey: "total",
    header: "Total",
    meta: { align: "right" },
    cell: ({ row }) =>
      new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        maximumFractionDigits: 0,
      }).format(row.original.total),
  },
]

function fetchOrders(
  sorting: SortingState,
  globalFilter: string,
  columnFilters: ColumnFiltersState,
  pagination: PaginationState,
  onDone: (rows: Order[], total: number) => void,
) {
  const timeout = setTimeout(() => {
    let rows = [...ALL_ORDERS]

    if (globalFilter) {
      const query = globalFilter.toLowerCase()

      rows = rows.filter((order) =>
        [order.number, order.customer]
          .join(" ")
          .toLowerCase()
          .includes(query),
      )
    }

    for (const filter of columnFilters) {
      if (filter.id === "status" && Array.isArray(filter.value)) {
        const allowed = filter.value as string[]

        rows = rows.filter((order) => allowed.includes(order.status))
      }
    }

    const sort = sorting[0]

    if (sort) {
      const key = sort.id as keyof Order

      rows.sort((first, second) => {
        const result =
          typeof first[key] === "number"
            ? (first[key] as number) - (second[key] as number)
            : String(first[key]).localeCompare(String(second[key]))

        return sort.desc ? -result : result
      })
    }

    const total = rows.length
    const pageStart = pagination.pageIndex * pagination.pageSize

    onDone(rows.slice(pageStart, pageStart + pagination.pageSize), total)
  }, 600)

  return () => clearTimeout(timeout)
}

export function DataTableServer() {
  const [sorting, setSorting] = React.useState<SortingState>([])
  const [globalFilter, setGlobalFilter] = React.useState("")
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
    [],
  )
  const [pagination, setPagination] = React.useState<PaginationState>({
    pageIndex: 0,
    pageSize: PAGE_SIZE,
  })
  const [result, setResult] = React.useState<{
    rows: Order[]
    total: number
    requestKey: string
  } | null>(null)

  const requestKey = JSON.stringify([
    sorting,
    globalFilter,
    columnFilters,
    pagination,
  ])
  const loading = result?.requestKey !== requestKey

  React.useEffect(() => {
    return fetchOrders(
      sorting,
      globalFilter,
      columnFilters,
      pagination,
      (rows, total) => {
        setResult({ rows, total, requestKey })
      },
    )
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [requestKey])

  return (
    <div className="w-full min-w-0 max-w-4xl">
      <DataTable
        columns={columns}
        data={result?.rows ?? []}
        caption="Orders"
        getRowId={(row) => row.id}
        search={{ placeholder: "Search orders...", debounceMs: 250 }}
        filters={[
          {
            columnId: "status",
            title: "Status",
            options: [
              { label: "Fulfilled", value: "Fulfilled" },
              { label: "Processing", value: "Processing" },
              { label: "Cancelled", value: "Cancelled" },
            ],
          },
        ]}
        rowCount={result?.total ?? 0}
        loading={loading}
        manualSorting
        manualFiltering
        manualPagination
        sorting={sorting}
        onSortingChange={(updater) => {
          const next = typeof updater === "function" ? updater(sorting) : updater

          setSorting(next)
          setPagination((previous) => ({ ...previous, pageIndex: 0 }))
        }}
        globalFilter={globalFilter}
        onGlobalFilterChange={(updater) => {
          const next =
            typeof updater === "function"
              ? updater(globalFilter)
              : String(updater)

          setGlobalFilter(next)
          setPagination((previous) => ({ ...previous, pageIndex: 0 }))
        }}
        columnFilters={columnFilters}
        onColumnFiltersChange={(updater) => {
          const next =
            typeof updater === "function" ? updater(columnFilters) : updater

          setColumnFilters(next)
          setPagination((previous) => ({ ...previous, pageIndex: 0 }))
        }}
        pagination={pagination}
        onPaginationChange={setPagination}
      />
    </div>
  )
}

```

## API reference [#api-reference]

### DataTable [#datatable]

| Prop                                                     | Type                                                   | Default             | Description                                                               |
| -------------------------------------------------------- | ------------------------------------------------------ | ------------------- | ------------------------------------------------------------------------- |
| `columns`                                                | `ColumnDef<TData>[]`                                   | required            | TanStack Table column definitions.                                        |
| `data`                                                   | `TData[]`                                              | required            | Row data for the current request or full dataset.                         |
| `table`                                                  | `Table<TData>`                                         | None                | Uses this TanStack Table instance instead of creating one.                |
| `caption`                                                | string                                                 | None                | Names the table for screen readers.                                       |
| `search`                                                 | boolean \| `{ placeholder?, debounceMs?, ariaLabel? }` | `false`             | Global search in the toolbar.                                             |
| `filters`                                                | `DataTableFilterConfig[]`                              | None                | Adds filters to the Filters menu.                                         |
| `selectable`                                             | boolean                                                | `false`             | Adds the selection checkbox column and summary.                           |
| `pagination`                                             | boolean \| `{ pageSizeOptions? }` \| `PaginationState` | `false`             | Footer pagination. Pass a `{ pageIndex, pageSize }` object to control it. |
| `pageSizeOptions`                                        | number\[]                                              | `[10, 20, 50, 100]` | Page-size choices for the footer select.                                  |
| `toolbarActions`                                         | ReactNode                                              | None                | Right-aligned slot for actions such as Export.                            |
| `density`                                                | `"default"` \| `"compact"`                             | `"default"`         | Compact reduces row padding for logs and admin screens.                   |
| `framed`                                                 | boolean                                                | `true`              | Draws the outer border. Disable when the layout already frames the table. |
| `getRowId`                                               | `(row, index) => string`                               | None                | Stable row identity. Use it with selection.                               |
| `getRowLabel`                                            | `(row) => string`                                      | None                | Returns the accessible label for each row checkbox.                       |
| `loading`                                                | boolean                                                | `false`             | Replaces the table body with skeleton rows.                               |
| `error`                                                  | string \| null                                         | `null`              | Shows an error row with a Try again action.                               |
| `onRetry`                                                | () => void                                             | None                | Runs when the user chooses Try again.                                     |
| `rowCount`                                               | number                                                 | filtered count      | Server-known total used in the result range.                              |
| `pageCount`                                              | number                                                 | computed            | Server-known page count for manual pagination.                            |
| `emptyState` / `noResultsState`                          | ReactNode                                              | built-in            | Replace either empty variant.                                             |
| `manualSorting` / `manualFiltering` / `manualPagination` | boolean                                                | `false`             | Makes the server responsible for the matching task.                       |

Controlled state follows TanStack Table's value and callback convention. The pairs are `sorting` and `onSortingChange`, `globalFilter` and `onGlobalFilterChange`, `columnFilters` and `onColumnFiltersChange`, `columnVisibility` and `onColumnVisibilityChange`, and `rowSelection` and `onRowSelectionChange`. Pagination uses `pagination` with `onPaginationChange`. If you omit a controlled value, Data Table manages that state.

### Column metadata [#column-metadata]

| Meta key | Values                              | Description                                                                            |
| -------- | ----------------------------------- | -------------------------------------------------------------------------------------- |
| `align`  | `"left"` \| `"right"` \| `"center"` | Cell and header alignment; use `"right"` for numbers.                                  |
| `label`  | string                              | Header text used by sort buttons and the Columns menu when the header is not a string. |
