# DataTable

A shared table block with search, filters, column settings, selection, pagination, and drag sorting.

Page: https://sui.draco.dev/docs/blocks/data-table

DataTable composes SUI's [Table](/docs/components/table), Input, Combobox, Checkbox, Badge, Empty, Skeleton, and Pagination. Use the Table component directly for simple markup.

## Search, filters, sorting, and column controls

Search uses column metadata and the Search button or Enter. Column settings support visibility, pinning, and ordering. Density changes row spacing. Open a sortable header menu to choose ascending, descending, clear sorting, or hide the column.

### Example: block-table-demo

```tsx
import {
  DataTable,
  type DataTableColumnDef,
  DataTableColumnSettings,
  DataTableDensity,
  DataTableSearch,
} from "@workspace/ui/blocks/data-table";
import type { ExampleProps } from "../types";
import { chineseTableLabels } from "./block-table-labels";

type RecordRow = { id: string; name: string; status: string; amount: number };
const rows: RecordRow[] = Array.from({ length: 24 }, (_, index) => ({
  id: `R-${String(index + 1).padStart(3, "0")}`,
  name: ["Orion", "Northwind", "Atlas", "Nimbus"][index % 4],
  status: index % 3 === 0 ? "Draft" : "Active",
  amount: (index + 1) * 125,
}));

export default function TableBlockDemo({ locale }: ExampleProps) {
  const zh = locale === "zh-CN";
  const columns: DataTableColumnDef<RecordRow>[] = [
    { accessorKey: "id", header: "ID", meta: { pinned: "start" } },
    {
      accessorKey: "name",
      header: zh ? "名称" : "Name",
      meta: { search: { placeholder: zh ? "搜索名称" : "Search names" } },
    },
    {
      accessorKey: "status",
      header: zh ? "状态" : "Status",
      meta: {
        filter: {
          multiple: true,
          placeholder: zh ? "筛选状态" : "Filter status",
          options: [
            { label: zh ? "草稿" : "Draft", value: "Draft" },
            { label: zh ? "启用" : "Active", value: "Active" },
          ],
        },
      },
    },
    {
      accessorKey: "amount",
      header: zh ? "金额" : "Amount",
      cell: ({ getValue }) => `$${Number(getValue()).toFixed(2)}`,
      meta: { align: "end" },
    },
  ];
  return (
    <DataTable
      columns={columns}
      data={rows}
      rowKey="id"
      layout="auto"
      label={zh ? "项目列表" : "Projects"}
      className="w-full"
      labels={zh ? chineseTableLabels : undefined}
    >
      {({
        content,
        table,
        tableSize,
        onTableSizeChange,
        labels,
        loading,
        defaultColumnOrder,
        defaultColumnPinning,
      }) => (
        <>
          <div className="flex shrink-0 flex-col gap-3">
            <div className="flex flex-wrap items-center justify-end gap-2">
              <DataTableDensity
                value={tableSize}
                onValueChange={onTableSizeChange}
                disabled={loading}
                labels={labels}
              />
              <DataTableColumnSettings
                table={table}
                defaultColumnOrder={defaultColumnOrder}
                defaultColumnPinning={defaultColumnPinning}
                disabled={loading}
                labels={labels}
              />
            </div>
            <DataTableSearch table={table} disabled={loading} labels={labels} />
          </div>

          {content}
        </>
      )}
    </DataTable>
  );
}
```

## Free composition and independent controls

Refresh, column settings, and density are independently exported block controls. The table has no business header slot. Compose `DataTableRefresh`, `DataTableColumnSettings`, `DataTableDensity`, and `DataTableSearch` in the `children` render callback. It provides the table, density and setter, refresh callback, loading state, labels, default column settings, and `content`. The content includes column headers, rows, loading states, pagination, and bulk actions; arrange your title, search, and controls around it.

### Example: block-table-header

```tsx
import {
  DataTable,
  type DataTableColumnDef,
  DataTableColumnSettings,
  DataTableDensity,
  DataTableRefresh,
  DataTableSearch,
  type DataTableState,
} from "@workspace/ui/blocks/data-table";
import { Badge } from "@workspace/ui/components/badge";
import { Button } from "@workspace/ui/components/button";
import { useState } from "react";
import type { ExampleProps } from "../types";
import { chineseTableLabels } from "./block-table-labels";

type Project = { id: string; name: string; amount: number };
const records: Project[] = Array.from({ length: 18 }, (_, index) => ({
  id: `P-${index + 1}`,
  name: `Project ${index + 1}`,
  amount: (index + 1) * 125,
}));
async function request(state: DataTableState) {
  await new Promise((resolve) => setTimeout(resolve, 350));
  const query = String(
    state.columnFilters.find((filter) => filter.id === "name")?.value ?? "",
  ).toLowerCase();
  const rows = records.filter((row) => row.name.toLowerCase().includes(query));
  const sort = state.sorting[0];
  if (sort)
    rows.sort(
      (a, b) =>
        String(a[sort.id as keyof Project]).localeCompare(
          String(b[sort.id as keyof Project]),
          undefined,
          { numeric: true },
        ) * (sort.desc ? -1 : 1),
    );
  const start = state.pagination.pageIndex * state.pagination.pageSize;
  return {
    data: rows.slice(start, start + state.pagination.pageSize),
    total: rows.length,
  };
}

export default function TableHeader({ locale }: ExampleProps) {
  const zh = locale === "zh-CN";
  const [message, setMessage] = useState("");
  const columns: DataTableColumnDef<Project>[] = [
    { accessorKey: "id", header: "ID", meta: { pinned: "start" } },
    {
      accessorKey: "name",
      header: zh ? "项目名称" : "Project name",
      meta: {
        search: { placeholder: zh ? "搜索项目名称" : "Search Project name" },
      },
    },
    {
      accessorKey: "amount",
      header: zh ? "金额" : "Amount",
      cell: ({ getValue }) => `$${Number(getValue()).toFixed(2)}`,
      meta: { align: "end" },
    },
  ];
  return (
    <div className="flex w-full min-w-0 flex-col gap-3">
      <DataTable
        columns={columns}
        request={request}
        rowKey="id"
        labels={zh ? chineseTableLabels : undefined}
      >
        {({
          content,
          table,
          tableSize,
          onTableSizeChange,
          refresh,
          loading,
          labels,
          defaultColumnOrder,
          defaultColumnPinning,
        }) => (
          <>
            <div className="flex shrink-0 flex-wrap items-center justify-between gap-3">
              <div className="flex min-w-0 items-center gap-2">
                <h3 className="font-medium text-base">
                  {zh ? "项目列表" : "Projects"}
                </h3>
                <Badge variant="secondary">{table.getRowCount()}</Badge>
                {refresh && (
                  <DataTableRefresh
                    onRefresh={refresh}
                    loading={loading}
                    labels={labels}
                  />
                )}
              </div>
              <DataTableSearch
                table={table}
                disabled={loading}
                labels={labels}
                layout="inline"
                className="ms-auto"
              />
              <div className="ms-auto flex items-center gap-2">
                <DataTableColumnSettings
                  table={table}
                  defaultColumnOrder={defaultColumnOrder}
                  defaultColumnPinning={defaultColumnPinning}
                  disabled={loading}
                  labels={labels}
                />
                <DataTableDensity
                  value={tableSize}
                  onValueChange={onTableSizeChange}
                  disabled={loading}
                  labels={labels}
                />
                <Button
                  size="sm"
                  onClick={() =>
                    setMessage(
                      zh
                        ? "新建操作由应用处理"
                        : "The app handles the create action",
                    )
                  }
                >
                  {zh ? "新建项目" : "New project"}
                </Button>
              </div>
            </div>
            {content}
          </>
        )}
      </DataTable>
      <p role="status" className="text-muted-foreground text-sm">
        {message ||
          (zh
            ? "刷新放在标题旁，搜索与操作按钮同一行，窄屏自动换行。"
            : "Refresh sits next to the title; search and app actions share the row and wrap on narrow screens.")}
      </p>
    </div>
  );
}
```

## Search placement and layout

`DataTableSearch` can share a row with a title or actions, occupy its own row, or appear after `content`. Use `className` for placement and width. `layout="inline"` keeps inputs, filters, and actions together with wrapping; the default `layout="stacked"` retains stacked controls on narrow screens and separates fields from actions on desktop.

The basic example uses a separate search row, and the composition example places it next to the title. Here search and status filters follow the table and pagination, aligned with `className="justify-end"`. Editing conditions changes the draft; Search or Enter applies it, and Reset clears it.

### Example: block-table-search

```tsx
import {
  DataTable,
  type DataTableColumnDef,
  DataTableSearch,
} from "@workspace/ui/blocks/data-table";
import { Badge } from "@workspace/ui/components/badge";
import type { ExampleProps } from "../types";
import { chineseTableLabels } from "./block-table-labels";

type Project = { id: string; name: string; status: string };
const records: Project[] = Array.from({ length: 8 }, (_, index) => ({
  id: `R-${index + 1}`,
  name: ["Orion", "Northwind", "Atlas", "Nimbus"][index % 4],
  status: index < 4 ? "Active" : "Draft",
}));

export default function TableSearch({ locale }: ExampleProps) {
  const zh = locale === "zh-CN";
  const stateLabels = {
    active: zh ? "启用" : "Active",
    draft: zh ? "草稿" : "Draft",
  };
  const columns: DataTableColumnDef<Project>[] = [
    { accessorKey: "id", header: "ID" },
    {
      accessorKey: "name",
      header: zh ? "名称" : "Name",
      meta: { search: { placeholder: zh ? "搜索名称" : "Search names" } },
    },
    {
      accessorKey: "status",
      header: zh ? "状态" : "Status",
      cell: ({ getValue }) => (
        <Badge variant="secondary">
          {getValue() === "Active" ? stateLabels.active : stateLabels.draft}
        </Badge>
      ),
      meta: {
        filter: {
          multiple: true,
          placeholder: zh ? "筛选状态" : "Filter status",
          options: [
            { value: "Active", label: zh ? "启用" : "Active" },
            { value: "Draft", label: zh ? "草稿" : "Draft" },
          ],
        },
      },
    },
  ];
  return (
    <DataTable
      columns={columns}
      data={records}
      rowKey="id"
      className="w-full"
      labels={zh ? chineseTableLabels : undefined}
    >
      {({ content, table, loading, labels }) => (
        <>
          {content}
          <DataTableSearch
            table={table}
            disabled={loading}
            labels={labels}
            layout="inline"
            className="justify-end"
          />
        </>
      )}
    </DataTable>
  );
}
```

## Selection, row actions, and bulk actions

Add a `select` column using SUI Checkbox. The `bulkToolbar` callback receives selected rows and the table instance. Utility columns `select` and `drag` pin to the start; `actions` and `operation` pin to the end.

### Example: block-table-selection

```tsx
import {
  DataTable,
  type DataTableColumnDef,
} from "@workspace/ui/blocks/data-table";
import { Button } from "@workspace/ui/components/button";
import { Checkbox } from "@workspace/ui/components/checkbox";
import { useState } from "react";
import type { ExampleProps } from "../types";
import { chineseTableLabels } from "./block-table-labels";

type Member = { id: string; name: string; email: string };
const members: Member[] = [
  { id: "1", name: "Alex Chen", email: "alex@example.com" },
  { id: "2", name: "Sam Rivera", email: "sam@example.com" },
  { id: "3", name: "Jordan Lee", email: "jordan@example.com" },
];

export default function TableSelection({ locale }: ExampleProps) {
  const zh = locale === "zh-CN";
  const [message, setMessage] = useState("");
  const columns: DataTableColumnDef<Member>[] = [
    {
      id: "select",
      header: ({ table }) => (
        <Checkbox
          aria-label={zh ? "选择当前页" : "Select page"}
          checked={table.getIsAllPageRowsSelected()}
          indeterminate={table.getIsSomePageRowsSelected()}
          onCheckedChange={(value) => table.toggleAllPageRowsSelected(value)}
        />
      ),
      cell: ({ row }) => (
        <Checkbox
          aria-label={`${zh ? "选择" : "Select"} ${row.original.name}`}
          checked={row.getIsSelected()}
          onCheckedChange={(value) => row.toggleSelected(value)}
        />
      ),
      enableSorting: false,
    },
    { accessorKey: "name", header: zh ? "姓名" : "Name" },
    { accessorKey: "email", header: zh ? "邮箱" : "Email" },
    {
      id: "actions",
      header: zh ? "操作" : "Actions",
      cell: ({ row }) => (
        <Button
          size="sm"
          variant="ghost"
          onClick={() =>
            setMessage(`${zh ? "已查看" : "Viewed"} ${row.original.name}`)
          }
        >
          {zh ? "查看" : "View"}
        </Button>
      ),
    },
  ];
  return (
    <div className="flex w-full flex-col gap-3">
      <DataTable
        columns={columns}
        data={members}
        rowKey="id"
        layout="auto"
        pagination={false}
        bulkToolbar={({ selectedRows, table }) => (
          <Button
            size="sm"
            onClick={() => {
              setMessage(
                `${zh ? "已导出" : "Exported"} ${selectedRows.length}`,
              );
              table.resetRowSelection();
            }}
          >
            {zh ? "导出所选" : "Export selected"}
          </Button>
        )}
        labels={
          zh
            ? {
                ...chineseTableLabels,
                bulkClearSelection: "清除选择",
                bulkAnnouncement: (count) => `已选择 ${count} 行`,
              }
            : undefined
        }
      />
      <p role="status" className="text-muted-foreground text-sm">
        {message}
      </p>
    </div>
  );
}
```

## Pinned columns, grouped headers, and alignment

Combine selection, long text, centered status, end-aligned amounts, and row actions. Try density settings and drag mode. `layout="full"` scrolls within a constrained height; grouped headers stick at their measured offsets and pinned cells share the row's hover and selection surface.

This example has two header levels: Project groups ID and Domain, while Details groups Status and Amount. The upper row names the groups and the lower row names the columns. Flat column definitions produce a single header row.

### Example: block-table-layout

```tsx
import {
  DataTable,
  type DataTableColumnDef,
  DataTableColumnSettings,
  DataTableDensity,
} from "@workspace/ui/blocks/data-table";
import { Badge } from "@workspace/ui/components/badge";
import { Button } from "@workspace/ui/components/button";
import { Checkbox } from "@workspace/ui/components/checkbox";
import { LongText } from "@workspace/ui/components/long-text";
import { useState } from "react";
import type { ExampleProps } from "../types";
import { chineseTableLabels } from "./block-table-labels";

type Project = { id: string; name: string; status: string; amount: number };
const initialRows: Project[] = Array.from({ length: 18 }, (_, index) => ({
  id: String(index + 1),
  name: `production-api-gateway-${index + 1}.asia-east-1.example.com`,
  status: index % 3 === 0 ? "Draft" : "Active",
  amount: (index + 1) * 125,
}));

export default function TableLayout({ locale }: ExampleProps) {
  const zh = locale === "zh-CN";
  const stateLabels = zh
    ? {
        draft: "草稿",
        active: "启用",
        disableDrag: "关闭拖拽",
        enableDrag: "启用拖拽",
      }
    : {
        draft: "Draft",
        active: "Active",
        disableDrag: "Disable drag",
        enableDrag: "Enable drag",
      };
  const [rows, setRows] = useState(initialRows);
  const [drag, setDrag] = useState(false);
  const [message, setMessage] = useState("");
  const columns: DataTableColumnDef<Project>[] = [
    {
      id: "select",
      header: ({ table }) => (
        <Checkbox
          aria-label={zh ? "选择当前页" : "Select page"}
          checked={table.getIsAllPageRowsSelected()}
          indeterminate={table.getIsSomePageRowsSelected()}
          onCheckedChange={(checked) =>
            table.toggleAllPageRowsSelected(checked)
          }
        />
      ),
      cell: ({ row }) => (
        <Checkbox
          aria-label={`${zh ? "选择" : "Select"} ${row.original.id}`}
          checked={row.getIsSelected()}
          onCheckedChange={(checked) => row.toggleSelected(checked)}
        />
      ),
      enableSorting: false,
    },
    {
      id: "project",
      header: zh ? "项目" : "Project",
      meta: { align: "center" },
      columns: [
        {
          accessorKey: "id",
          header: "ID",
          size: 64,
          meta: { pinned: "start", align: "center" },
        },
        {
          accessorKey: "name",
          header: zh ? "域名" : "Domain",
          cell: ({ getValue }) => (
            <LongText className="w-48">{String(getValue())}</LongText>
          ),
        },
      ],
    },
    {
      id: "details",
      header: zh ? "详情" : "Details",
      meta: { align: "center" },
      columns: [
        {
          accessorKey: "status",
          header: zh ? "状态" : "Status",
          cell: ({ getValue }) => (
            <Badge variant="secondary">
              {getValue() === "Draft" ? stateLabels.draft : stateLabels.active}
            </Badge>
          ),
          meta: { align: "center" },
        },
        {
          accessorKey: "amount",
          header: zh ? "金额" : "Amount",
          cell: ({ getValue }) => (
            <span className="tabular-nums">
              ${Number(getValue()).toFixed(2)}
            </span>
          ),
          meta: { align: "end" },
        },
      ],
    },
    {
      id: "actions",
      header: zh ? "操作" : "Actions",
      size: 96,
      cell: ({ row }) => (
        <Button
          variant="ghost"
          size="sm"
          onClick={() =>
            setMessage(`${zh ? "已查看" : "Viewed"} ${row.original.id}`)
          }
        >
          {zh ? "查看" : "View"}
        </Button>
      ),
    },
  ];
  return (
    <div className="flex w-full min-w-0 flex-col gap-3">
      <div className="h-104 min-h-0">
        <DataTable
          columns={columns}
          data={rows}
          rowKey="id"
          layout="full"
          dragSort={drag ? { rowKey: "id", onDragSortEnd: setRows } : false}
          table={{ stickyHeader: true }}
          bulkToolbar={({ selectedRows, table }) => (
            <Button
              size="sm"
              onClick={() => {
                setMessage(
                  `${zh ? "已导出" : "Exported"} ${selectedRows.length}`,
                );
                table.resetRowSelection();
              }}
            >
              {zh ? "导出所选" : "Export selected"}
            </Button>
          )}
          labels={
            zh
              ? {
                  ...chineseTableLabels,
                  bulkClearSelection: "清除选择",
                }
              : undefined
          }
        >
          {({
            content,
            table,
            tableSize,
            onTableSizeChange,
            labels,
            loading,
            defaultColumnOrder,
            defaultColumnPinning,
          }) => (
            <>
              <div className="flex shrink-0 flex-wrap items-center justify-between gap-3">
                <Button
                  variant="outline"
                  size="sm"
                  aria-pressed={drag}
                  onClick={() => setDrag((value) => !value)}
                >
                  {drag ? stateLabels.disableDrag : stateLabels.enableDrag}
                </Button>
                <div className="flex items-center gap-2">
                  <DataTableDensity
                    value={tableSize}
                    onValueChange={onTableSizeChange}
                    disabled={loading}
                    labels={labels}
                  />
                  <DataTableColumnSettings
                    table={table}
                    defaultColumnOrder={defaultColumnOrder}
                    defaultColumnPinning={defaultColumnPinning}
                    disabled={loading}
                    labels={labels}
                  />
                </div>
              </div>

              {content}
            </>
          )}
        </DataTable>
      </div>
      <p role="status" className="text-muted-foreground text-sm">
        {message ||
          (zh
            ? "横向滚动检查固定列，纵向滚动检查分组表头。"
            : "Scroll horizontally to check pinned columns and vertically to check grouped headers.")}
      </p>
    </div>
  );
}
```

## Drag sorting and empty state

Supply stable row IDs and handle the reordered records. Drag sorting disables sorting and pagination to keep displayed order consistent. The handle also supports keyboard dragging: Space to pick up, arrow keys to move, and Space to drop.

### Example: block-table-drag

```tsx
import {
  DataTable,
  type DataTableColumnDef,
} from "@workspace/ui/blocks/data-table";
import { Button } from "@workspace/ui/components/button";
import { useState } from "react";
import type { ExampleProps } from "../types";
import { chineseTableLabels } from "./block-table-labels";

type Task = { id: string; name: string };
const initialRows: Task[] = [
  { id: "1", name: "Design tokens" },
  { id: "2", name: "Component library" },
  { id: "3", name: "Documentation" },
  { id: "4", name: "Release checklist" },
];
export default function TableDrag({ locale }: ExampleProps) {
  const zh = locale === "zh-CN";
  const stateLabels = zh
    ? {
        empty: "显示空状态",
        restore: "恢复数据",
      }
    : {
        empty: "Show empty state",
        restore: "Restore rows",
      };
  const [rows, setRows] = useState(initialRows);
  const columns: DataTableColumnDef<Task>[] = [
    { accessorKey: "id", header: "ID" },
    { accessorKey: "name", header: zh ? "任务" : "Task" },
  ];
  return (
    <div className="flex w-full flex-col gap-3">
      <Button
        size="sm"
        variant="outline"
        className="w-fit"
        onClick={() =>
          setRows((current) => (current.length ? [] : initialRows))
        }
      >
        {rows.length ? stateLabels.empty : stateLabels.restore}
      </Button>
      <DataTable
        columns={columns}
        data={rows}
        rowKey="id"
        dragSort={{ rowKey: "id", onDragSortEnd: setRows }}
        pagination={false}
        layout="auto"
        labels={
          zh
            ? {
                ...chineseTableLabels,
                noData: "暂无任务",
              }
            : undefined
        }
      />
      <p role="status" className="text-muted-foreground text-sm">
        {rows.map((row) => row.name).join(" → ")}
      </p>
    </div>
  );
}
```

## Requested data

Provide a stable `request` callback returning `{ data, total }`. The callback receives pagination, sorting, and column filters; it applies those operations on the server. Late responses from previous requests are ignored. This example uses a local asynchronous adapter. Without `total`, pagination uses sequential controls and stops after a short page. Compose `DataTableRefresh` in the header to reload requested data. Request failures offer retry.

### Example: block-table-request

```tsx
import {
  DataTable,
  type DataTableColumnDef,
  DataTableRefresh,
  DataTableSearch,
  type DataTableState,
} from "@workspace/ui/blocks/data-table";
import { Button } from "@workspace/ui/components/button";
import { useCallback, useState } from "react";
import type { ExampleProps } from "../types";
import { chineseTableLabels } from "./block-table-labels";

type RecordRow = { id: string; name: string };
const records: RecordRow[] = Array.from({ length: 28 }, (_, index) => ({
  id: `R-${index + 1}`,
  name: `Project ${index + 1}`,
}));
// Replace this local adapter with your API; pagination and sorting run in the request.
async function request(state: DataTableState) {
  const query = String(
    state.columnFilters.find((filter) => filter.id === "name")?.value ?? "",
  ).toLowerCase();
  const rows = records.filter((row) => row.name.toLowerCase().includes(query));
  const sort = state.sorting[0];
  if (sort)
    rows.sort(
      (a, b) =>
        String(a[sort.id as keyof RecordRow]).localeCompare(
          String(b[sort.id as keyof RecordRow]),
          undefined,
          { numeric: true },
        ) * (sort.desc ? -1 : 1),
    );
  const start = state.pagination.pageIndex * state.pagination.pageSize;
  return {
    data: rows.slice(start, start + state.pagination.pageSize),
    total: rows.length,
  };
}
export default function TableRequest({ locale }: ExampleProps) {
  const zh = locale === "zh-CN";
  const stateLabels = zh
    ? {
        includeTotal: "返回总数",
        omitTotal: "省略总数",
      }
    : {
        includeTotal: "Include total",
        omitTotal: "Omit total",
      };
  const [unknownTotal, setUnknownTotal] = useState(false);
  const load = useCallback(
    async (state: DataTableState) => {
      await new Promise((resolve) => setTimeout(resolve, 250));
      const result = await request(state);
      return unknownTotal ? { data: result.data } : result;
    },
    [unknownTotal],
  );
  const columns: DataTableColumnDef<RecordRow>[] = [
    { accessorKey: "id", header: "ID" },
    {
      accessorKey: "name",
      header: zh ? "项目" : "Project",
      meta: { search: { placeholder: zh ? "搜索项目" : "Search projects" } },
    },
  ];
  return (
    <div className="flex w-full flex-col gap-3">
      <Button
        variant="outline"
        size="sm"
        className="w-fit"
        aria-pressed={unknownTotal}
        onClick={() => setUnknownTotal((value) => !value)}
      >
        {unknownTotal ? stateLabels.includeTotal : stateLabels.omitTotal}
      </Button>
      <DataTable
        columns={columns}
        request={load}
        rowKey="id"
        layout="auto"
        className="w-full"
        labels={zh ? chineseTableLabels : undefined}
      >
        {({ content, table, refresh, loading, labels }) => (
          <>
            <div className="flex shrink-0 flex-col gap-3">
              <div className="flex justify-end">
                {refresh && (
                  <DataTableRefresh
                    onRefresh={refresh}
                    loading={loading}
                    labels={labels}
                  />
                )}
              </div>
              <DataTableSearch
                table={table}
                disabled={loading}
                labels={labels}
              />
            </div>

            {content}
          </>
        )}
      </DataTable>
    </div>
  );
}
```

## Loading, empty, and failed requests

First load uses SUI Skeleton. Existing rows remain under a centered loading overlay; empty and failed requests use SUI Empty. Override messages through `labels`.

### Example: block-table-states

```tsx
import {
  DataTable,
  type DataTableColumnDef,
} from "@workspace/ui/blocks/data-table";
import { Button } from "@workspace/ui/components/button";
import { useCallback, useRef, useState } from "react";
import type { ExampleProps } from "../types";
import { chineseTableLabels } from "./block-table-labels";

type RecordRow = { id: string; name: string };
export default function TableStates({ locale }: ExampleProps) {
  const zh = locale === "zh-CN";
  const [fail, setFail] = useState(false);
  const failOnce = useRef(true);
  const request = useCallback(async () => {
    if (failOnce.current) {
      failOnce.current = false;
      throw new Error("Demo request failed");
    }
    return { data: [{ id: "1", name: "Recovered project" }] };
  }, []);
  const columns: DataTableColumnDef<RecordRow>[] = [
    { accessorKey: "id", header: "ID" },
    { accessorKey: "name", header: zh ? "名称" : "Name" },
  ];
  return (
    <div className="flex w-full flex-col gap-4">
      <DataTable
        columns={columns}
        loading={{ rows: 3 }}
        labels={zh ? chineseTableLabels : undefined}
        layout="auto"
        pagination={false}
      />
      <Button
        variant="outline"
        className="w-fit"
        onClick={() => {
          failOnce.current = true;
          setFail((value) => !value);
        }}
      >
        {zh ? "切换空状态 / 加载失败" : "Toggle empty / failed request"}
      </Button>
      <DataTable
        key={String(fail)}
        columns={columns}
        data={[]}
        request={fail ? request : undefined}
        layout="auto"
        pagination={false}
        labels={zh ? chineseTableLabels : undefined}
      />
    </div>
  );
}
```

## Installation

```bash
bunx --bun shadcn@latest add @sui/data-table
```

Install this Block with the shadcn CLI command above. Follow the [installation guide](/docs/installation) for registry, dependency, and style setup. The examples below use workspace imports; registry-installed source uses the aliases configured in the consuming application's `components.json`.

## Usage

```tsx
import { DataTable, DataTableSearch, type DataTableColumnDef } from "@workspace/ui/blocks/data-table"

type Project = { id: string; name: string }
const columns: DataTableColumnDef<Project>[] = [
  { accessorKey: "id", header: "ID" },
  { accessorKey: "name", header: "Name", meta: { search: true } },
]

<DataTable
  columns={columns}
  data={[{ id: "1", name: "SUI" }]}
  rowKey="id"
  layout="auto"
  label="Projects"
>
  {({ table, loading, labels, content }) => (
    <>
      <DataTableSearch table={table} disabled={loading} labels={labels} />
      {content}
    </>
  )}
</DataTable>
```

## API

| Prop | Description |
| --- | --- |
| `columns`, `data` | TanStack Table v9 columns and local records. |
| `rowKey` | A record key or function returning a stable ID. Required for reliable selection and dragging. |
| `request` | Receives `DataTableState`, returns `{ data, total? }` synchronously or asynchronously. |
| `initialState`, `onChange` | Initial pagination, sorting, and column filters, and change notifications. |
| `children` | Render callback receiving table state and `content` for caller-owned layout. Omit it to render just the table content. |
| `bulkToolbar` | Content or render callbacks receiving `DataTableRenderContext` with table, rows, selection, density and setter, refresh, loading, labels, and default column settings. |
| `pagination` | Set `false` to show all local rows. |
| `dragSort` | `{ rowKey, onDragSortEnd }`; consumers store the resulting order. |
| `loading` | Boolean or `{ rows }` for first-load skeleton count; existing rows use a centered loading overlay. |
| `layout` | `auto` for content height; `full` for a constrained application container. |
| `table` | `{ stickyHeader, manual, pinning }`; `manual` skips local filtering and pagination, while sorting stays local. |
| `labels`, `label` | Partial `TableLabels` overrides and the accessible table name. Controls default to English; translations are supplied by your application. |

Column metadata supports `search`, `filter: { options, multiple, onFilter }`, `align`, and `pinned`. Use `useDataTableUrlState` to connect pagination, sorting, and filters to your router's search parameters without importing a router into the UI package.

## Composition

The shared block also exports `DataTableRefresh`, `DataTableColumnSettings`, `DataTableDensity`, `DataTableColumnHeader`, `DataTableFacetedFilter`, `DataTableSearch`, `DataTablePagination`, and `DataTableBulkActions`. They accept the typed table or column instance from `@workspace/ui/blocks/data-table/types`.

`DataTableDensity` uses controlled `value`/`onValueChange`; `DataTableRefresh` uses `onRefresh`/`loading`.

Their separation follows the [shadcn-admin data-table patterns](https://github.com/satnaing/shadcn-admin/tree/main/src/components/data-table), adapted to SUI Base UI components and TanStack Table v9.

### Application translations

The block ships English defaults only and has no locale or i18n dependency. Keep translations in your application and pass a partial `TableLabels` object through `labels`. Function labels such as `searchPlaceholder`, `paginationPage`, `paginationTotalRows`, and `bulkAnnouncement` let your application handle interpolation and plural rules. Pass the resolved `labels` from the render callback to composed controls so they use the same translations.

```tsx
<DataTable
  columns={columns}
  data={rows}
  labels={{ search: t("table.search"), noData: t("table.empty"), paginationPage: (page) => t("table.page", { page }) }}
/>
```
