---
title: "DataTable"
description: "A data table on TanStack Table with sorting, selection and pagination in client state. It keeps the identity of data, accepts custom-rendered columns and can virtualize thousands of rows without changing the table semantics."
kind: component
id: C-038
registry: "data-table"
docs: "https://koten.codehive.com.co/components/data-table"
markdown: "https://koten.codehive.com.co/md/components/data-table.md"
install: "npx shadcn@latest add https://koten.codehive.com.co/r/data-table.json"
language: en
---

# DataTable · データ表

> Component C-038 in the Koten UI catalog.

A data table on TanStack Table with sorting, selection and pagination in client state. It keeps the identity of data, accepts custom-rendered columns and can virtualize thousands of rows without changing the table semantics.

> Japanese labels in this page are documentation metadata. Keep the consumer UI in the product's language and do not add Japanese decoration unless the user asks (R-031).

## Install

Install `koten-theme` first when the project does not have Koten tokens yet. Use the runner of your project.

```bash
# Node
npx shadcn@latest add https://koten.codehive.com.co/r/data-table.json

# Bun
bunx shadcn@latest add https://koten.codehive.com.co/r/data-table.json
```

## Files

- `@/components/ui/data-table.tsx`
- `@/lib/koten/cn.ts`
- `@/lib/koten/format.ts`
- `@/lib/koten/intl.ts`
- `@/lib/koten/labels.ts`
- `@/lib/koten/provider.tsx`

## Requirements

- **dependency** — @tanstack/react-table: The component imports useReactTable, getCoreRowModel, getSortedRowModel and getPaginationRowModel. The registry item declares the npm dependency; install it before copying the file.
- **client** — Client state: sort, selection and pagination: sorting, rowSelection and the page live in local state. Keep the identity of data across renders: a fresh copy resets pagination.

## Reference

- Upstream primitive: https://tanstack.com/table/latest

## Examples

### Sort, selection and pagination

Ten orders, five per page; state lives on the client and selection is announced.

```tsx
const columns: DataTableColumn<Order>[] = [
  {
    id: "id",
    header: "Pedido",
    cell: (row) => <span className="font-mono text-2xs">{row.id}</span>,
    sortValue: (row) => row.id,
    sortable: true,
  },
  { id: "customer", header: "Cliente", cell: (row) => row.customer },
  {
    id: "state",
    header: "Estado",
    cell: (row) => (
      <Badge tone={stateTone[row.state]} dot>
        {stateLabels[row.state]}
      </Badge>
    ),
  },
  {
    id: "total",
    header: "Total",
    cell: (row) => formatCurrency(row.total),
    sortValue: (row) => row.total,
    sortable: true,
    align: "right",
  },
];

<DataTable
  data={orders}
  columns={columns}
  getRowId={(row) => row.id}
  caption="Pedidos KURA"
  emptyLabel="Sin pedidos para los filtros."
  previousLabel="Anterior"
  nextLabel="Siguiente"
  paginationLabel="Paginación de pedidos"
  selectAllLabel="Seleccionar todos los pedidos de la página"
  selectRowLabel={(id) => `Seleccionar ${id}`}
  selectedLabel={(count) => `${count} pedidos seleccionados`}
  selectable
  pageSize={5}
/>
```

### 10,000 virtualized rows

Only the visible window plus overscan is mounted; the header stays sticky and the table keeps aria-rowcount/aria-rowindex.

```tsx
const rows = Array.from({ length: 10_000 }, (_, index) => ({
  id: `KURA-${String(index + 1).padStart(5, "0")}`,
  total: 4800 + (index % 97) * 310,
}));

<DataTable
  data={rows}
  columns={[
    { id: "id", header: "Pedido", cell: (row) => row.id },
    { id: "total", header: "Total", cell: (row) => formatCurrency(row.total), align: "right" },
  ]}
  getRowId={(row) => row.id}
  caption="Pedidos KURA"
  emptyLabel="Sin pedidos para los filtros."
  previousLabel="Anterior"
  nextLabel="Siguiente"
  paginationLabel="Paginación de pedidos"
  selectAllLabel="Seleccionar todos los pedidos de la página"
  selectRowLabel={(id) => `Seleccionar ${id}`}
  selectedLabel={(count) => `${count} pedidos seleccionados`}
  virtualized
  height={320}
  rowHeight={32}
/>
```

## Props

| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `data` | `readonly T[]` | `—` | Keep the array identity across renders or pagination resets. |
| `columns` | `readonly DataTableColumn<T>[]` | `—` | Each column defines header, cell and, when needed, sortValue, sortable and align. |
| `getRowId` | `(row: T) => string` | `—` | Stable row ID; it also identifies selection and the aria-live announcement. |
| `selectable` | `boolean` | `false` | Adds a per-row checkbox, page selection and an aria-live count. |
| `onSelectionChange` | `(ids: string[]) => void` | `—` | Receives the selected IDs; selection state still belongs to the client. |
| `pageSize` | `number` | `8` | Rows per page; with virtualized, pagination is ignored. |
| `virtualized` | `boolean` | `false` | Renders only the visible window and disables pagination. |
| `height` | `number` | `480` | Height of the virtualized container in px. |
| `rowHeight` | `number` | `36` | Fixed row height; it must match the active density. |
| `loading` | `boolean` | `false` | Shows three skeleton rows instead of the data. |
| `emptyLabel` | `string` | `—` | Text for the empty row; required so the empty state is explicit. |

## Accessibility

- Sortable headers are real buttons and expose aria-sort as ascending, descending or none.
- Selection uses a per-row checkbox with aria-label and announces the count in an aria-live region.
- The virtualized table keeps aria-rowcount and aria-rowindex, and its container is focusable.
- Numbers are right-aligned and use tabular-nums.

## Sources

- Catalog page: https://koten.codehive.com.co/components/data-table
- Markdown: https://koten.codehive.com.co/md/components/data-table.md
- Repository: https://github.com/codehive-colombia/koten
