<Table /> instead.
You work with two pieces:
- The hook,
experimental_useDataTable({data, columns, getRowId}), owns the data: the rows, the columns, and what each cell shows. Onlydatais required. See Columns from the data. - The component the hook returns,
<DataTable />, owns how the table looks:height,emptyStateandselectable.
Example
Parameters
Returns
Columns
Each column is defined by: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 nocell.cell, the visual representation of the value fromaccessor.
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.valueis 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
rowinstead, sincerowkeeps 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.
Text
Return a string or a number to show text:- Returns: a
stringor anumber. A boolean or aDatefromcellis not text, convert it first, likevalue ? "Yes" : "No"orIntl.DateTimeFormat. - No
cellneeded whenaccessoralready returns a string, a number, a boolean, aDateornull. See Columns. - Sorts and aligns by the
accessorvalue, not by the text."12 seats"still sorts as12. 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. cellcan’t return the boolean itself. Return a badge, or text likevalue ? "Yes" : "No".- Sorts by the
accessorvalue,falsebeforetrue, so “No” comes before “Yes”.
Badge
Use a badge for a short label, like a plan or a team:- Props:
children, the label text, andcolor, one ofamber,blue,cyan,green,grey,lavender,lime,orange,pink,purple,redoryellow. See<Badge />. childrenmust be text. Wrap a non-string value in a template string, like{`${value}`}.- Sorts by the
accessorvalue, so return the label itself fromaccessor, 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:
colorandchildren, both required.coloris 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
accessorvalue, 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 benullorundefined. See<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:
- Props:
href, the URL, andchildren, 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.
Attio link
Use<AttioLink /> to open a place inside Attio, like the member’s record page:
- Props:
to, a destination fromDestinations, likeDestinations.record(record), andchildren, the link text. See<AttioLink />. - Sorts by the
accessorvalue. 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:
labelandonClick, both required.variant,disabledandiconare optional. See<Button />. onClickruns in your app with therowfromcellin 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 setenableSorting: false, since there is nothing to sort by. - For an action on many rows, use
selectableandselectedRowIdsinstead.
Loading state
Show a spinner while a value is still loading:- No props needed. See
<LoadingState />. - Return
undefinedfromaccessorwhile 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
dataarray. The selection and the sort stay.
Empty cells
Returnnull or undefined to leave a cell empty:
nullandundefinedshow the same empty cell.- Rows where
accessorreturnsnullorundefinedsort 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:falseis skipped, socondition && <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 whataccessor returns, not from what cell shows. So you can format a number into a string and the
column stays right aligned:
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 outcolumns 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 doescolumns: []. The table then shows a blank row for each row indata, with no headers. Aselectabletable shows no checkboxes, since they sit in the first column. - The header is the key in sentence case:
firstName,first_nameandfirst-nameall read “First name”. Acronyms lose their capitals, soURLreads “Url”. - The column
idis the raw key, sosorting.idis 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
cellto show the object another way. - Without
getRowId, a row’s id is its position indata.
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
accessorvalue, not by whatcellshows, so aDatesorts by time. Empty values,NaNincluded, always go last. A column where no row has a string, number, boolean orDateto sort by, like a column of arrays, can’t be sorted. - Selection. Follows the ids from
getRowId. A row stays selected through sorting and newdataarrays, and drops out ofselectedRowIdsonce its id is gone. An inlinefilterormapis 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
Limits
- No custom cells. Cells travel to Attio as data, so
cellcan 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.