BetaData display

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.

PropTypeRequiredDescription
variantTableVariant | undefinedNo
sizeTableSize | undefinedNoThe row's height, the cell's inset and the type.
radiusRadiusKey | undefinedNoThe shell's corner.
colorstring | undefinedNoA raw tint (R7). It lands on a chosen row and on the sort mark.
selectionModeSelectionMode | undefinedNo
selectedKeysreadonly string[] | undefinedNoThe chosen rows, by their `Table.Row` id. Present means controlled.
defaultSelectedKeysreadonly string[] | undefinedNo
onSelectionChange((keys: readonly string[]) => void) | undefinedNo
disabledKeysreadonly string[] | undefinedNoRows that cannot be chosen, and which the header's box does not count.
sortDescriptorSortDescriptor | undefinedNoWhich 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) | undefinedNo
isDisabledboolean | undefinedNoDims the whole table and stops every press.
styleStyleProp<ViewStyle>No
childrenReactNodeNo

Slots

TableColumnProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
idstring | undefinedNoNames the column in `sortDescriptor`. Required to sort on it.
widthnumber | undefinedNoFixed width in points. Unset, the column takes an equal share of the row.
allowsSortingboolean | undefinedNoWhether pressing the header sorts on it.
childrenReactNodeNo

TableRowProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
idstring | undefinedNoWhat this row is called in `selectedKeys`. Required to choose it.
childrenReactNodeNo

TableScrollProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

TableTextProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

TableViewProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

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

The source defines no extra rule. Keep the React Native labels, roles, states and focus order your case needs, then check the result with VoiceOver and TalkBack.

Migration from legacy

This component declares no rule of its own. The migration guide covers variants, colours and slots.

Implementation notes

Three nodes, and each earns its place

  • Table is the shell: the fill, the border, the corner, the shadow. It clips, and it does not move.
  • Table.ScrollContainer is the horizontal scroller.
  • Table.Content is 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.

  • single replaces, 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.