Skip to main content
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 /> 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.
  2. The component the hook returns, <DataTable />, owns how the table looks: height, emptyState and selectable.

Example

Parameters

Returns

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.
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.
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: 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.
Don’t do this:
Do this instead:
The examples below use a table of team members.

Text

Return a string or a number to show text:
  • 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.
  • Sorts and aligns by the accessor value, not by the text. "12 seats" still sorts as 12. See Number alignment.

Booleans

Show a boolean as a badge with “Yes” or “No”:
  • 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:
  • Props: children, the label text, and color, one of amber, blue, cyan, green, grey, lavender, lime, orange, pink, purple, red or yellow. See <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.

Status badge

Use a status badge for a state, like active or invited:
  • Props: color and children, both required. color is a status indicator color. See <StatusBadge />.
  • Use it for a value with a few fixed states, like a status or a boolean. For free text like a tag, use <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:
  • Props: name, required, shows beside the avatar. src, the image URL, can be null or undefined. See <Avatar />.
  • An avatar has no value to sort by on its own. Return the name from accessor, so the column sorts by name.
Use <Link /> for a page outside Attio:
  • Props: href, the URL, and children, the link text. Both required. See <Link />.
  • For a page inside Attio, like a record, use <AttioLink /> instead.
  • Can share a cell with text and <AttioLink />. See Several items.
Use <AttioLink /> to open a place inside Attio, like the member’s record page:
  • Props: to, a destination from Destinations, like Destinations.record(record), and children, the link text. See <AttioLink />.
  • 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:
  • Props: label and onClick, both required. variant, disabled and icon are optional. See <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:
  • No props needed. See <LoadingState />.
  • 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:
  • 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:
The same cell as a Fragment:
  • 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:
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.
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:
  • 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

Each height is a maximum number of rows, not a fixed height: 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

  • 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. 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.