Balance Logo
Balance
Reckon Design System
Open playroom

Table

Tables are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data.
Install
pnpm add @balance-web/table
Import usage
import {
Table,
TableCell,
TableColumnHeader,
TableDragCell,
TableGroup,
TableGroupHeader,
TableBody,
TableFooter,
TableFoot,
TableHeader,
TableHead,
TableRow,
TableSelectionCell,
TableEmptyState,
TableRowDragBox,
createDefaultSort,
useNestingLeftOffset
} from '@balance-web/table';
  • Code
  • API
Edit in Playroom

Concepts

This package contains many low-level "primitive" components, which can be composed together to achieve the desired table interface.

Primitives

Primitives are "dumb" components that assume as little as possible about their usage, they don't own any state and have no side-effects, making them easier to understand and maintain for our team.

Primitives allow us to move quickly and build the best abstraction for each case. While the approach to composition may vary depending on requirements and constraints, primitives offer familiarity along with flexibility.

<Table>
<TableHeader>
<TableColumnHeader />
</TableHeader>
<TableBody>
<TableRow>
<TableCell />
</TableRow>
</TableBody>
<TableFooter>
<TableCell />
</TableFooter>
</Table>

Motivation

Monolithic components don't know your application's constraints and requirements. Assumptions must be made about how a component will be used, which often results in some less-than-ideal implementation artefacts:

  • increased API surface-area i.e. endless prop configuration
  • limited opportunity for performance improvements
  • inability to support changes in design requirements

Appearance

Cell appearance

The TableCell component accepts props to adust appearance:

  • align — Sets the horizontal alignment of cell contents.
  • maxWidth — Sets the maximum width of the cell.
  • minWidth — Sets the minimum width of the cell.
  • width — Sets the width of the cell.
  • showDivider — Whether the cell should show a divider between it and the next cell. When using sticky cells dividers are shown by default.

Row appearance

The TableRow component accepts props to adust appearance:

  • status — Sets the interaction status of the row.
  • tone — Sets tone of the row, influences the background colour.
  • disabled - Makes the row as disabled. Can be used while dragging or selecting where some rows aren't meant to be interacted with.

Table appearance

The root Table component accepts props to infuence the appearance of every cell:

  • alignY — Sets the vertical alignment for content within each cell. Only relevant when allowing text content to wrap.
  • density — Sets the amount of vertical padding within each cell.
  • overflowStrategy — Sets the overflow strategy for Text content within each cell.

Sticky behaviour

The stickyOffset property applies "sticky" positioning to the element, offset relative to its nearest scrolling ancestor.

Sticky rows

The TableHeader and TableFooter may be "sticky" along the vertical axis:

  • The TableHeader will be offset against the top.
  • The TableFooter will be offset against the bottom.

Sticky cells

The TableColumnHeader and TableCell may be "sticky" along the horizontal axis:

  • Positive values e.g. 0, 240 will offset the cell against the left.
  • Negative values e.g. -0, -240 will offset the cell against the right.

Providing a stickyOffset will default the showDivider property to true.

Custom content

There's some optimisations provided out-of-the-box that are only supported with default usage. When providing ReactText (string and number) content to cells TableColumnHeader and TableCell everything will behave as expected.

Text behaviour

Custom implementations, depending on design requirements, can adjust Text appearance and behaviour to match the table defaults.

Memoization

Custom cell content should be memoized with useMemo for simple cases and memo for complex component cases.

Sorting

The TableColumnHeader accepts an onClick prop, which should be used for sorting. When a click handler is provided you must also provide aria-sort to the actively sorted column, which will render the appropriate arrow indicator and let users of assistive tech know how the data within the table is sorted.

Default Sort

The table package exposes a createDefaultSort utility, which will handle most sorting cases.

type SortDescriptor = {
column: string;
direction: 'ascending' | 'descending';
};

The utility takes a SortDescriptor as its only argument and returns a compare function that should be provided to the sort method of your data array.

const rowData = [
{ name: 'Brig Joderli', email: 'bjoderli0@nymag.com', },
{ name: 'Jill Rowledge', email: 'jrowledge1@amazon.co.uk', },
];
rowData.sort(createDefaultSort({
column: 'name';
direction: 'descending';
}));

Selection

Use the TableSelectionCell to implement checkbox selection of rows—internally it composes a TableCell of standard width and checkbox for consistent appearance and behaviour across products.

Indeterminate state

The indeterminate state of a checkbox input element is controlled by JavaScript, not HTML attribution. To make things more declarative and less confusing Balance checkboxes accept a "tri-state"; the checked value for a checkbox may be true | false | 'mixed', where "mixed" indicates an indeterminate state aligning with the aria-checked attribute.

Conditional elements

Some tables may have many columns and/or many rows. Loading a subset of columns or rows, via mechanisms like virtualization and pagination, can improve performance and user experience.

Low-level primitives

The TableHeader and TableFooter are provided as convenience components for the most common cases. Internally they compose the TableHead and TableFoot with TableRow, which are exposed for cases like conditionally rendered elements.

<TableHead>
<TableRow>
...
</TableRow>
</TableHead>
...
<TableFoot>
<TableRow>
...
</TableRow>
</TableFoot>

Conditional rows

When only a subset of rows are loaded, you need to let all users know which rows are being displayed.

On the Table, use the aria-rowcount attribute to let assistive technologies know how many rows the table would have if all rows were present. For paginated results, consider providing an aria-description or aria-describedby attribute to announced the current state.

On the TableRow, use the aria-rowindex attribute to let assistive technologies know where each row is in relation to the total possible rows.

Conditional columns

When only a subset of rows are loaded, you need to let all users know which columns are being displayed.

On the Table, use the aria-colcount attribute to let assistive technologies know how many columns the table would have if all columns were present.

On the TableCell and TableColumnHeader, use the aria-colindex attribute to let assistive technologies know where each column is in relation to the total possible columns.

Groups

Groups are non-standard table structure and should be used sparingly.

Tables that implement groups have no TableBody, instead a number of TableGroup components should be rendered as siblings of the TableHeader.

The TableGroupHeader must be given an aria-colspan matching the total number of columns.

Empty state

The TableEmptyState component can be used instead of TableBody when there is no data available.

Copyright © 2024 Reckon. Designed and developed in partnership with Thinkmill.
Bitbucket logoJira software logoConfluence logo