Table
Rows and columns, with a shell round them.
Overview
Rows and columns, with a shell round them.
First example
<Table selectionMode="multiple" selectedKeys={keys} onSelectionChange={setKeys}><Table.ScrollContainer><Table.Content minWidth={520}><Table.Header><Table.SelectAllCell /><Table.Column id="name" allowsSorting>Nom</Table.Column><Table.Column id="role" width={140}>Rôle</Table.Column></Table.Header><Table.Body>{people.map(person => (<Table.Row key={person.id} id={person.id}><Table.SelectionCell /><Table.Cell>{person.name}</Table.Cell><Table.Cell>{person.role}</Table.Cell></Table.Row>))}</Table.Body></Table.Content></Table.ScrollContainer><Table.Footer>…</Table.Footer></Table>
Anatomy
<Table>
<Table.Column />
<Table.Row />
<Table.Scroll />
<Table.Text />
<Table.View />
</Table>
Usage
<Table selectionMode="multiple" selectedKeys={keys} onSelectionChange={setKeys}>
<Table.ScrollContainer>
<Table.Content minWidth={520}>
<Table.Header>
<Table.SelectAllCell />
<Table.Column id="name" allowsSorting>
Nom
</Table.Column>
<Table.Column id="role" width={140}>
Rôle
</Table.Column>
</Table.Header>
<Table.Body>
{people.map(person => (
<Table.Row key={person.id} id={person.id}>
<Table.SelectionCell />
<Table.Cell>{person.name}</Table.Cell>
<Table.Cell>{person.role}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Content>
</Table.ScrollContainer>
<Table.Footer>…</Table.Footer>
</Table>
Root props
TableProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| variant | TableVariant | undefined | No | — |
| size | TableSize | undefined | No | The row's height, the cell's inset and the type. |
| radius | RadiusKey | undefined | No | The shell's corner. |
| color | string | undefined | No | A raw tint (R7). It lands on a chosen row and on the sort mark. |
| selectionMode | SelectionMode | undefined | No | — |
| selectedKeys | readonly string[] | undefined | No | The chosen rows, by their `Table.Row` id. Present means controlled. |
| defaultSelectedKeys | readonly string[] | undefined | No | — |
| onSelectionChange | ((keys: readonly string[]) => void) | undefined | No | — |
| disabledKeys | readonly string[] | undefined | No | Rows that cannot be chosen, and which the header's box does not count. |
| sortDescriptor | SortDescriptor | undefined | No | Which column the data is sorted on. **The table never reorders anything.** It reports the press and the caller sorts their own collection — a table that sorted for you would need to understand every cell's value, and the only thing that does is the code that built the row. |
| onSortChange | ((descriptor: SortDescriptor | undefined) => void) | undefined | No | — |
| isDisabled | boolean | undefined | No | Dims the whole table and stops every press. |
| style | StyleProp<ViewStyle> | No | — |
| children | ReactNode | No | — |
Slots
TableColumnProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| id | string | undefined | No | Names the column in `sortDescriptor`. Required to sort on it. |
| width | number | undefined | No | Fixed width in points. Unset, the column takes an equal share of the row. |
| allowsSorting | boolean | undefined | No | Whether pressing the header sorts on it. |
| children | ReactNode | No | — |
TableRowProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| id | string | undefined | No | What this row is called in `selectedKeys`. Required to choose it. |
| children | ReactNode | No | — |
TableScrollProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
TableTextProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
TableViewProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
Variants, sizes and colour
Variants
primary is a raised card, for a table among other things on a page. secondary is flat —
the page's own ground with a filled header band — for a table that is the screen.
No success or danger: a table reports what is in it, and the intent belongs to a Chip in
a cell rather than to the shell round all of them.
The tint does not touch the shell
color lands on a chosen row and on the sort mark. It is kept off the root explicitly,
because bg names surface — a bare token — and resolveTint would map it to the tint like
any other, turning a blue app's table entirely blue. A tint on a container means the thing it
marks, not the ground everything sits on.
Size
size moves the row's height, the cell's inset, the type and the shell's corner. The height
is fixed: a value too long is truncated rather than deforming the table.
The corner sits a level below the Card's at the same size, and radius overrides it. A card
is one padded surface and can take a wide curve; a table is a stack of square rows behind a
shell that clips them, so past lg the top row's corner starts eating into the first cell's
text while every row underneath stays flat.
Accessibility
Migration from legacy
Implementation notes
Three nodes, and each earns its place
Tableis the shell: the fill, the border, the corner, the shadow. It clips, and it does not move.Table.ScrollContaineris the horizontal scroller.Table.Contentis the column inside it, and the node that is allowed to be wider than the shell.
Folding them together is what makes a wide table either clip its own rows or drag its border across the screen. The header is inside the scroller with the body, because a header that stayed put while its cells moved sideways would be a header naming the wrong values.
Table.Footer is outside the scroller: a pager that slid off with a wide table would be
a pager nobody could reach.
Widths are declared by the column and read by position
Table.Column registers its width at its index; Table.Cell reads the width at its own.
Neither names the other, and a table stays aligned because both run the same two lines
against the same number:
width === undefined ? { flex: 1 } : { width }
A column with no width takes an equal share of what is left. Table.Content's minWidth is
what stops flexible columns squeezing below a legible width on a narrow phone.
The space between columns is the row's gap, not a padding on the cell — a padding would
double at the table's two edges, and none at all lets a name and a role run together the
moment a flexible column shrinks to its content.
The table never reorders anything
Sorting reports the press and the caller sorts their own collection:
const sorted = useMemo(() => {
if (sort === undefined) return people
const way = sort.direction === 'ascending' ? 1 : -1
return [...people].sort((a, b) => String(a[sort.column]).localeCompare(…) * way)
}, [people, sort])
A table that sorted for you would have to understand every cell's value, and the only thing that does is the code that built the row.
Three presses, not two. A new column starts ascending, the same column turns round, and a descending column pressed again clears the sort — every table that cannot get back to its own order makes a reader reload the screen to do it.
Selection
selectionMode is none, single or multiple; rows are named by their id and reported
as an array of keys.
singlereplaces, and pressing the chosen row again clears it. A single-selection list with no way back to none is a list a reader can only get wrong once.- The header's box has three states — nothing, some, all. One that only knew the first two would tell a reader who has chosen four of twenty rows that they have chosen none.
- It counts only rows that can be chosen, so a table with a disabled row still has a box that can be filled.
- It keeps keys chosen outside this table. One page of a filtered list must not clear a choice made on another.
Table.SelectionCell reads its row rather than its position, so it works wherever in the
row it is written — a table whose boxes are on the trailing edge is the same JSX with the slot
written last. Pressing the row does the same thing, deliberately: a box the size of a
fingertip inside a row the size of a hand should not be the only way to choose one.
There is no `virtualized` prop
A table of ten thousand rows is a FlatList, and Table.Body takes asChild so it is
one:
<Table.Body asChild>
<FlatList
data={people}
keyExtractor={person => person.id}
renderItem={({ item }) => <Table.Row id={item.id}>…</Table.Row>}
/>
</Table.Body>
A prop would have meant this component owning a data and a renderItem — the shape the
whole v1 API was written to get away from — and owning them badly, since a virtualized list
has a dozen props a table would then forward one by one.
The rules are hairlines
Every line inside the table — under the header, between two rows, above the footer, and
between two column names — is one device pixel, StyleSheet.hairlineWidth, as the List's
and the Menu's are. Only the shell's own outline is a full point.
The rule between column names is the header's alone: a row's gap already says where one
field ends, and a grid ruled in both directions is a spreadsheet. It is drawn out of flow, so
it costs no width and a header column still starts on the same edge as the cells under it.
See also
List— for rows that are not a grid.Chip— what a status cell usually holds.