> ## Documentation Index
> Fetch the complete documentation index at: https://docs.attio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# experimental_useDataTable()

> Show many rows in a fast, scrollable table that the viewer can sort, select and resize.

```js theme={"system"}
import {experimental_useDataTable} from "attio/client"
```

> [!WARNING]
>
> This hook is experimental. Its API can change in a minor release of the SDK.

A data table shows many rows, even thousands, and stays fast because Attio only draws the rows on
screen. The viewer can sort, select rows and resize columns. For a small table that never changes,
use [`<Table />`](./table) instead.

You work with two pieces:

1. **The hook**, `experimental_useDataTable({data, columns, getRowId})`, owns the data: the rows,
   the columns, and what each cell shows. Only `data` is required. See
   [Columns from the data](#columns-from-the-data).
2. **The component** the hook returns, `<DataTable />`, owns how the table looks: `height`,
   `emptyState` and `selectable`.

## Example

```tsx theme={"system"}
import {experimental_useDataTable} from "attio/client"

export function TeamTable({members}: {members: Array<{id: string; name: string; seats: number}>}) {
  const {DataTable} = experimental_useDataTable({
    data: members,
    getRowId: (member) => member.id,
    columns: [
      {id: "name", header: "Name", accessor: (member) => member.name},
      {id: "seats", header: "Seats", accessor: (member) => member.seats},
    ],
  })

  return <DataTable emptyState="No members yet" />
}
```

## Parameters

| Param | Type | Required | Description |
| - | - | - | - |
| `data` | `Array<TRow>` | Yes | The data shown in the table. Each element is one row, in the order the table starts in. |
| `columns` | `Array<DataTableColumn<TRow>>` | No | The columns, in display order. See [Columns](#columns). Defaults to one column per key on the rows, see [Columns from the data](#columns-from-the-data). |
| `getRowId` | `(row: TRow) => string` | No | Returns a stable, unique id for a row. Selection uses these ids. Defaults to the row's position in `data`, as a string: `"0"`, `"1"` and so on. |

## Returns

| Field | Type | Description |
| - | - | - |
| `DataTable` | `(props: DataTableProps) => JSX.Element` | The table, already bound to your rows and columns. Render it where the table should appear. See [`<DataTable />` props](#datatable-props). |
| `selectedRowIds` | `Array<string>` | The ids of the selected rows, in the order the table shows them. Always empty unless `<DataTable />` is `selectable`. |
| `clearSelection` | `() => void` | Unselects every row. |
| `sorting` | `{id: string \| null, direction: "asc" \| "desc" \| null}` | The `id` of the column the viewer sorted by, and the direction. Both are `null` when the rows are in your order. Read only: Attio sorts the rows for you. |

## Columns

Each column is defined by:

1. **`accessor`**, a function that reads a value from the row. Every column needs one. Its return
   value is used to sort the column, to decide the alignment of the cell content, and as the text of
   the cell when there is no `cell`.
2. **`cell`**, the visual representation of the value from `accessor`.

```tsx theme={"system"}
const columns: Array<DataTableColumn<Member>> = [
  // Shows the name as plain text. No `cell` needed.
  {id: "name", header: "Name", accessor: (member) => member.name},

  // Sorts by the raw number, but shows it with a word after it, like "12 seats".
  {
    id: "seats",
    header: "Seats",
    icon: "Users",
    size: 120,
    accessor: (member) => member.seats,
    cell: ({value}) => `${value} seats`,
  },

  // Shows an avatar. `accessor` still returns the name, so the column sorts by name.
  {
    id: "member",
    header: "Member",
    icon: "User",
    accessor: (member) => member.name,
    cell: ({row}) => <Avatar name={row.name} src={row.avatarUrl} />,
  },

  // A button per row. There is nothing to sort by, so sorting is off.
  {
    id: "remove",
    header: "",
    size: 80,
    enableSorting: false,
    accessor: (member) => member.id,
    cell: ({row}) => <Button label="Remove" onClick={() => removeMember(row.id)} />,
  },
]
```

| Prop | Type | Required | Description |
| - | - | - | - |
| `id` | `string` | Yes | A stable id, unique in the table. `sorting` reports the column by this id. A duplicate logs a warning. |
| `header` | `string` | Yes | The text in the column header. |
| `accessor` | `(row: TRow) => unknown` | Yes | Reads the column's value from the row. |
| `cell` | `({row, value}) => DataTableCellNode` | Sometimes | What the cell shows. See [Cells](#cells) and the rule below. |
| `icon` | `Icon` | No | An [Attio icon](/sdk/icons.md) in the column header. |
| `size` | `number` | No | The starting width in pixels. Defaults to `200`. |
| `enableSorting` | `boolean` | No | Lets the viewer sort by clicking the header. Defaults to `true`. |

`cell` is optional when `accessor` returns a string, a number, a boolean, a `Date`, `null` or
`undefined`, since those show as text: a boolean as "true" or "false" and a `Date` as ISO text.
`NaN` and an invalid `Date` leave the cell empty. Anything else needs a `cell`, and TypeScript
rejects the column without one.

> [!NOTE]
>
> `cell` gets two arguments: `row`, the whole row, and `value`, what `accessor` returned for it.
> TypeScript types `value` as `unknown`, so you can't call methods on it.
>
> * `value` is enough to put it in text or compare it: `` `${value} seats` ``, `value === "active"`.
> * To call a method or format a number, read the field from `row` instead, since `row` keeps its
>   types: `row.tags.map(...)`, `currency.format(row.arr)`.

## Cells

`cell` has to return one of these:

| Return | Shows |
| - | - |
| `string` or `number` | Text |
| [`<Badge />`](./badge) | A badge |
| [`<StatusBadge />`](./status-badge) | A status badge |
| [`<Avatar />`](./avatar) | An avatar with the name beside it |
| [`<Link />`](./link) | A link to an external URL |
| [`<AttioLink />`](../navigation/attio-link) | A link to a place inside Attio, like a record page |
| [`<Button />`](./button) | A button |
| [`<LoadingState />`](./loading-state) | A spinner, for a value that is still loading |
| `null` or `undefined` | An empty cell |
| An array or a Fragment of the above | Several items in one cell |

Nothing else can be shown:

* A cell holds one item from this list. For several, return an array or a Fragment, not a
  wrapper component.
* Components can't be nested. Their children can only be text, so a `<Badge />` inside a
  `<Link />` doesn't work.
* Your own components can't be used.

Any of these leaves the cell empty, and the console logs a warning naming the column.

> [!WARNING]
>
> Don't do this:
>
> ```tsx theme={"system"}
> // A component inside another component
> cell: ({row}) => (
>   <Link href={row.website}>
>     <Badge color="blue">{row.name}</Badge>
>   </Link>
> )
>
> // Your own component
> cell: ({row}) => <MemberCard member={row} />
> ```
>
> Do this instead:
>
> ```tsx theme={"system"}
> cell: ({row}) => <Link href={row.website}>{row.name}</Link>
> ```

The examples below use a table of team members.

### Text

Return a string or a number to show text:

```tsx theme={"system"}
const seats: DataTableColumn<Member> = {
  id: "seats",
  header: "Seats",
  accessor: (member) => member.seats,
  cell: ({value}) => `${value} seats`,
}
```

* Returns: a `string` or a `number`. A boolean or a `Date` from `cell` is not text, convert it
  first, like `value ? "Yes" : "No"` or `Intl.DateTimeFormat`.
* No `cell` needed when `accessor` already returns a string, a number, a boolean, a `Date` or
  `null`. See [Columns](#columns).
* Sorts and aligns by the `accessor` value, not by the text. `"12 seats"` still sorts as `12`. See
  [Number alignment](#number-alignment).

### Booleans

Show a boolean as a badge with "Yes" or "No":

```tsx theme={"system"}
const isActive: DataTableColumn<Member> = {
  id: "isActive",
  header: "Active",
  accessor: (member) => member.isActive,
  cell: ({value}) => (value ? <Badge color="green">Yes</Badge> : <Badge color="grey">No</Badge>),
}
```

* Without `cell`, a boolean shows as the text "true" or "false", which reads like code.
* `cell` can't return the boolean itself. Return a badge, or text like `value ? "Yes" : "No"`.
* Sorts by the `accessor` value, `false` before `true`, so "No" comes before "Yes".

### Badge

Use a badge for a short label, like a plan or a team:

```tsx theme={"system"}
const plan: DataTableColumn<Member> = {
  id: "plan",
  header: "Plan",
  accessor: (member) => member.plan,
  cell: ({value}) => <Badge color="blue">{`${value}`}</Badge>,
}
```

* Props: `children`, the label text, and `color`, one of `amber`, `blue`, `cyan`, `green`, `grey`,
  `lavender`, `lime`, `orange`, `pink`, `purple`, `red` or `yellow`. See [`<Badge />`](./badge).
* `children` must be text. Wrap a non-string value in a template string, like ``{`${value}`}``.
* Sorts by the `accessor` value, so return the label itself from `accessor`, not a color.
* For several badges in one cell, return an array or a Fragment. See [Several items](#several-items).

### Status badge

Use a status badge for a state, like active or invited:

```tsx theme={"system"}
const status: DataTableColumn<Member> = {
  id: "status",
  header: "Status",
  accessor: (member) => member.status,
  cell: ({value}) =>
    value === "active" ? (
      <StatusBadge color="green">Active</StatusBadge>
    ) : (
      <StatusBadge color="grey">Invited</StatusBadge>
    ),
}
```

* Props: `color` and `children`, both required. `color` is a status indicator color. See
  [`<StatusBadge />`](./status-badge).
* Use it for a value with a few fixed states, like a status or a boolean. For free text like a
  tag, use [`<Badge />`](#badge).
* Sorts by the `accessor` value, so `"active"` sorts before `"invited"`, whatever the labels say.

### Avatar

Use an avatar for a person or a company. The name shows beside it:

```tsx theme={"system"}
const member: DataTableColumn<Member> = {
  id: "member",
  header: "Member",
  accessor: (member) => member.name,
  cell: ({row}) => <Avatar name={row.name} src={row.avatarUrl} />,
}
```

* Props: `name`, required, shows beside the avatar. `src`, the image URL, can be `null` or
  `undefined`. See [`<Avatar />`](./avatar).
* An avatar has no value to sort by on its own. Return the name from `accessor`, so the column
  sorts by name.

### Link

Use `<Link />` for a page outside Attio:

```tsx theme={"system"}
const website: DataTableColumn<Member> = {
  id: "website",
  header: "Website",
  accessor: (member) => member.website,
  cell: ({row}) => <Link href={row.website}>{row.website}</Link>,
}
```

* Props: `href`, the URL, and `children`, the link text. Both required. See [`<Link />`](./link).
* For a page inside Attio, like a record, use [`<AttioLink />`](#attio-link) instead.
* Can share a cell with text and `<AttioLink />`. See [Several items](#several-items).

### Attio link

Use `<AttioLink />` to open a place inside Attio, like the member's record page:

```tsx theme={"system"}
const name: DataTableColumn<Member> = {
  id: "name",
  header: "Name",
  accessor: (member) => member.name,
  cell: ({row}) => <AttioLink to={Destinations.record(row.record)}>{row.name}</AttioLink>,
}
```

* Props: `to`, a destination from `Destinations`, like `Destinations.record(record)`, and
  `children`, the link text. See [`<AttioLink />`](../navigation/attio-link).
* Sorts by the `accessor` value. Return the link text, here the name, so the column sorts by what
  the viewer reads.

### Button

Use a button for an action on one row:

```tsx theme={"system"}
const remove: DataTableColumn<Member> = {
  id: "remove",
  header: "",
  size: 80,
  enableSorting: false,
  accessor: (member) => member.id,
  cell: ({row}) => <Button label="Remove" onClick={() => removeMember(row.id)} />,
}
```

* Props: `label` and `onClick`, both required. `variant`, `disabled` and `icon` are optional. See
  [`<Button />`](./button).
* `onClick` runs in your app with the `row` from `cell` in scope, so read the row's id there.
* Several buttons in one cell, returned as an array or a Fragment, share a "More actions" menu.
* Every column needs an `accessor`, even a column of buttons. Return the row id and set
  `enableSorting: false`, since there is nothing to sort by.
* For an action on many rows, use `selectable` and `selectedRowIds` instead.

### Loading state

Show a spinner while a value is still loading:

```tsx theme={"system"}
const openDeals: DataTableColumn<Member> = {
  id: "openDeals",
  header: "Open deals",
  accessor: (member) => member.openDeals,
  cell: ({row}) => (row.openDeals === undefined ? <LoadingState /> : row.openDeals),
}
```

* No props needed. See [`<LoadingState />`](./loading-state).
* Return `undefined` from `accessor` while the value loads. Empty values sort last, so loading rows
  go to the bottom when the viewer sorts.
* Once the value arrives, hand over a new `data` array. The selection and the sort stay.

### Empty cells

Return `null` or `undefined` to leave a cell empty:

```tsx theme={"system"}
const manager: DataTableColumn<Member> = {
  id: "manager",
  header: "Manager",
  accessor: (member) => member.managerName,
  cell: ({row}) => (row.managerName ? <Badge color="grey">{row.managerName}</Badge> : null),
}
```

* `null` and `undefined` show the same empty cell.
* Rows where `accessor` returns `null` or `undefined` sort last and don't change the column's
  alignment.
* To show that a value is missing on purpose, return text like `"N/A"` instead.

### Several items

Return an array or a Fragment to show several items in one cell, like a badge per role:

```tsx theme={"system"}
const roles: DataTableColumn<Member> = {
  id: "roles",
  header: "Roles",
  accessor: (member) => member.team,
  cell: ({row}) => [
    row.isAdmin && <Badge color="red">Admin</Badge>,
    <Badge color="blue">{row.team}</Badge>,
  ],
}
```

The same cell as a Fragment:

```tsx theme={"system"}
cell: ({row}) => (
  <>
    {row.isAdmin && <Badge color="red">Admin</Badge>}
    <Badge color="blue">{row.team}</Badge>
  </>
)
```

* `false` is skipped, so `condition && <Badge />` works.
* Keep one kind of item per cell. Text, `<Link />` and `<AttioLink />` count as one kind, so they
  can mix: they show on one line, separated by commas, and stay left aligned even in a column of
  numbers.
* Any other mix keeps the kind of the first item, drops the rest and logs a warning.

## Number alignment

A column of numbers is right aligned, so the digits line up. Attio decides this from what
`accessor` returns, not from what `cell` shows. So you can format a number into a string and the
column stays right aligned:

```tsx theme={"system"}
const winRate: DataTableColumn<Deal> = {
  id: "winRate",
  header: "Win rate",
  accessor: (deal) => deal.winRate,
  cell: ({row}) => new Intl.NumberFormat(undefined, {style: "percent"}).format(row.winRate),
}
```

The first row with a value decides for the whole column: right aligned when its `accessor` returned
a number, left aligned otherwise. Rows where `accessor` returned `null` or `undefined` are skipped,
so an `"N/A"` for a missing amount doesn't change the alignment. Keep one kind of value per column.

> [!NOTE]
>
> Return the raw number from `accessor`, not a string you already formatted. An accessor returning
> `"$36k"` is text, so the column is left aligned. For an Attio currency value, return the amount
> (`currency_value`) and read the currency code from `row` in `cell`.

## Columns from the data

Leave out `columns` and every key on the rows becomes a column:

```tsx theme={"system"}
const {DataTable} = experimental_useDataTable({
  data: [
    {name: "Ada", age: 36, skills: ["math", "poetry"]},
    {name: "Grace", age: 85, active: true},
  ],
})
```

* Keys are read from every row, not only the first, in the order they first appear. A key only some
  rows have still gets a column.
* Rows with no keys, like `{}`, give no columns, and so does `columns: []`. The table then shows a
  blank row for each row in `data`, with no headers. A `selectable` table shows no checkboxes,
  since they sit in the first column.
* The header is the key in sentence case: `firstName`, `first_name` and `first-name` all read "First
  name". Acronyms lose their capitals, so `URL` reads "Url".
* The column `id` is the raw key, so `sorting.id` is the key you know.
* Strings and numbers show as text, booleans as "true" or "false", dates as ISO text and arrays as
  a list.
* Objects can't be shown yet. A cell holding an object, or an array holding one, shows an "Error"
  label, and the console logs an error naming the column. The rest of the table renders as normal.
  Give the column a `cell` to show the object another way.
* Without `getRowId`, a row's id is its position in `data`.

Inferred columns are for a quick look at the data. Write `columns` to choose headers, format values
or show components.

## `<DataTable />` props

| Prop | Type | Default | Description |
| - | - | - | - |
| `height` | `"small" \| "medium" \| "large" \| "extraLarge"` | `"medium"` | The tallest the table can grow. See below. |
| `emptyState` | `string` | `"No results"` | The text shown when the table has no rows. |
| `selectable` | `boolean` | `false` | Adds a checkbox to every row and a select all to the first header. The selected rows come back on `selectedRowIds`. |

Each `height` is a maximum number of rows, not a fixed height:

| `height` | Rows |
| - | - |
| `small` | 6 |
| `medium` | 10 |
| `large` | 14 |
| `extraLarge` | 20 |

The header sits above these rows. A table with fewer rows is only as tall as its rows, but never
shorter than 3 rows. Extra rows scroll.

## Sorting, selection and column widths

Attio owns sorting and selection. Your app reads the result, and never sorts the rows itself.

* **Sorting.** Clicking a header sorts ascending, then descending, then back to your order. Rows
  sort by the `accessor` value, not by what `cell` shows, so a `Date` sorts by time. Empty values,
  `NaN` included, always go last. A column where no row has a string, number, boolean or `Date` to
  sort by, like a column of arrays, can't be sorted.
* **Selection.** Follows the ids from `getRowId`. A row stays selected through sorting and new
  `data` arrays, and drops out of `selectedRowIds` once its id is gone. An inline `filter` or `map`
  is fine.
* **Column widths.** The viewer can drag any column except the last to a width between 80 and 800
  pixels. Double click the edge to reset it to `size`.

## Best practices

> [!TIP]
>
> * **Give each column a short, stable `id`**, like `"arr"`. Your app reads `sorting.id`, so keep it
>   the same when you rename the header.
> * **Use a real, stable id in `getRowId`**, like a record id. Selection follows these ids, and the
>   default is the row's position, which points at the wrong rows once rows are added, removed or
>   reordered.
> * **Hand over rows in the order they should start in.** Clicking past descending goes back to it.
> * **Return the raw value from `accessor`, format it with `Intl` in `cell`.** The column sorts and
>   aligns by the real value, and `Intl` uses the viewer's locale and timezone when you leave the
>   locale out.

## Limits

* **No custom cells.** Cells travel to Attio as data, so `cell` can only return what is in
  [Cells](#cells). Anything else shows an empty cell and logs a warning, and components can't be
  nested.
* **No filtering.** Filter in your app and hand over a new array.
* **Every update sends every row.** You can't update a single row, so a change to one row resends
  the whole table.
* **Column widths are not saved** between mounts.
* **Fixed error state.** If the table fails to render, it shows "This table could not be
  displayed". You can't change the text, and your app is not told.
* **Web only.** The mobile app and the browser extension don't show data tables yet.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.