---
title: Data table
description: A table described by column definitions. Sorting, selection, expandable rows and a layout that adapts to its container.
---

```tsx
import { DataTable } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { COLUMNS } from "./columns";
import { ROWS } from "./rows";

export default function DataTableBasic() {
  return (
    <Preview block={true}>
      <DataTable columns={COLUMNS} data={ROWS} />
    </Preview>
  );
}
```

- Use it for a list of records with the same columns. Use the [Table](/deft/components/table) parts directly for grouped rows or rows that differ in shape.
- Columns are TanStack Table column definitions; `meta` sets how a column looks and how it adapts to narrow containers.
- Give every column a fixed `width` except the one that takes the remaining space, usually the `title` column, so header and rows line up.
- Pass `data` in display order. The table never sorts or filters it.
- Rows are keyed by `getRowId`, or by a string `id` on the row. `getRowId` is required when rows have no string `id`.

## Sorting, selection and links

```tsx
import { DataTable, type DataTableSorting } from "@openperf/deft";
import { useState } from "react";

import { Preview } from "../../src/Preview";
import { COLUMNS } from "./columns";
import { ROWS } from "./rows";

export default function DataTableSortingSelectionAndLinks() {
  const [sorting, setSorting] = useState<DataTableSorting["state"]>([{ id: "value", desc: true }]);
  const [selected, setSelected] = useState<ReadonlySet<string>>(new Set());

  return (
    <Preview block={true}>
      <DataTable
        columns={COLUMNS}
        data={sorting[0]?.desc ? ROWS.toReversed() : ROWS}
        link={(row) => ({ href: `#${row.id}` })}
        selection={{ selected, onChange: setSelected }}
        sorting={{ state: sorting, onChange: setSorting }}
      />
    </Preview>
  );
}
```

`sorting` makes the columns that have an `accessorFn` sortable. The first click sorts descending, the next ascending. `sorting.onChange` receives the next `SortingState` and the caller loads the data in that order.

`selection` adds a checkbox column with select-all; Shift-click selects a range. `link` turns the `title` column into the row's destination; `onRowClick` is for a row action without one.

## Expandable rows

```tsx
import { DataTable } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { COLUMNS } from "./columns";
import { ROWS } from "./rows";

export default function DataTableExpandable() {
  return (
    <Preview block={true}>
      <DataTable columns={COLUMNS} data={ROWS} renderDetail={(row) => `Details of ${row.name}`} />
    </Preview>
  );
}
```

`renderDetail` adds a chevron column and renders its result under an open row. A click anywhere on the row opens it, and `onRowClick` is ignored. Cells read `row.getIsExpanded()` to change their content when the row is open.

## Loading and empty

```tsx
import { DataTable } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { COLUMNS } from "./columns";

export default function DataTableStates() {
  return (
    <Preview block={true}>
      <DataTable columns={COLUMNS} data={[]} loadingRows={3} pending={true} />
      <DataTable
        columns={COLUMNS}
        data={[]}
        empty={{ title: "Title", description: "Description" }}
      />
    </Preview>
  );
}
```

`pending` replaces the body with `loadingRows` skeleton rows. After a successful load with no rows, `empty` fills the body; omit it while the request is failing.

## Adaptive layout

The table reacts to the width of its container, not the viewport.

- In the compact layout, columns with `priority: "low"` are hidden and `compactWidth` replaces `width`.
- In the stacked layout, the narrowest, the header is hidden and each row becomes a two-line card. The `title` and `aside` cells share the first line, `meta` cells wrap onto a smaller second line, and `lead` cells (checkbox, chevron, status icon) sit in a gutter on the left. Number cells on the second line are prefixed with their header. `priority: "medium"` hides visual-only columns, such as meters, in this layout.

## Column kinds

| Kind      | Area    | Defaults                                       |
| --------- | ------- | ---------------------------------------------- |
| `title`   | `title` | Medium weight; wrapped by `link`.              |
| `number`  | `meta`  | End-aligned tabular figures, `secondary` tone. |
| `date`    | `aside` | End-aligned tabular figures, `icon` tone.      |
| `actions` | `aside` | End-aligned.                                   |
| `text`    | `meta`  | The default kind.                              |

Set `area`, `align`, `tone` or `weight` in `meta` to override a kind's default, for example to move a key metric to `aside`.

## API

| Prop | Type | Default | Description |
| - | - | - | - |
| `columns` | `DataTableColumnDef<TData>[]` | - | |
| `data` | `readonly TData[]` | - | Rows in display order. |
| `getRowId?` | `(row: TData) => string` | - | Required when rows have no string id. |
| `columnVisibility?` | `Record<string, boolean>` | - | Hides columns by id regardless of width. |
| `sorting?` | `{ state: SortingState; onChange: (next: SortingState) => void }` | - | Manual sorting. |
| `selection?` | `{ selected: ReadonlySet<string>; onChange: (next: Set<string>) => void }` | - | Row selection by row id. |
| `link?` | `(row: TData) => TableRowLinkProps \| undefined` | - | Row destination: href, or render for a router link. |
| `onRowClick?` | `(row: TData) => void` | - | Row action without a link. Ignored with renderDetail. |
| `renderDetail?` | `(row: TData) => ReactNode` | - | Expandable row panel. |
| `pending?` | `boolean` | - | Shows skeleton rows. |
| `loadingRows?` | `number` | `8` | Skeleton row count. |
| `empty?` | `{ title: ReactNode; description?: ReactNode; actions?: ReactNode }` | - | Empty state. |
| `hideHeader?` | `boolean` | - | Omits the header row. |
| `aria-label?` | `string` | - | Names the table when no heading does. |
| `density?` | `"default" \| "compact" \| "dense"` | `"default"` | Row height. |

### Column meta

| Prop | Type | Default | Description |
| - | - | - | - |
| `kind?` | `"title" \| "number" \| "date" \| "actions" \| "text"` | `"text"` | |
| `area?` | `"lead" \| "title" \| "aside" \| "meta"` | - | Place in the stacked layout. |
| `width?` | `number \| string` | - | A number is in pixels, a string is a grid track. Omitted, the column takes the remaining space. |
| `compactWidth?` | `number \| string` | - | Replaces width in the compact layout. |
| `priority?` | `"high" \| "medium" \| "low"` | - | Which layouts hide the column. |
| `align?` | `"start" \| "end"` | - | |
| `tone?` | `"default" \| "secondary" \| "muted" \| "icon"` | - | |
| `weight?` | `"regular" \| "medium"` | - | |
| `truncate?` | `boolean` | - | Cuts long content with an ellipsis. |

Built on [Table](/deft/components/table), [Checkbox](/deft/components/checkbox) and [Empty](/deft/components/empty), with [TanStack Table](https://tanstack.com/table) for the row model.
