Table
When to use
Section titled “When to use”- Records compared across the same columns: invoices, users
- Data people sort or scan by column
- Totals in a footer
Not for layout, or items that read better as tiles — use a Card grid.
Choosing between organising content:
| Component | Use it for | For example |
|---|---|---|
| Accordion | Sections stacked on one page, of which people read one or a few | FAQ, settings groups, order details |
| Tabs | Views of the same thing at the same level, one at a time | Account / Billing / Security |
| Card | A self-contained group of content, shown as one unit, often in a grid or list | A product tile, a project summary |
| Table | Rows of records compared across the same columns | Invoices, users, order lines |
| Separator | A line that divides sections, with no content of its own | Between menu groups or toolbar groups |
| Invoice | Customer | Status | |
|---|---|---|---|
| INV-1042 | Ada Lovelace | Paid | €1,200 |
| INV-1043 | Alan Turing | Due | €450 |
| INV-1044 | Grace Hopper | Paid | €3,100 |
| INV-1045 | Katherine Johnson | Overdue | €780 |
| Total | €5,530 | ||
import { useState } from 'react';import { Button } from '@arun-dev/headless/button';import { Table } from '@arun-dev/ui';
const invoices = [ { id: 'INV-1042', customer: 'Ada Lovelace', amount: 1200, status: 'Paid' }, { id: 'INV-1043', customer: 'Alan Turing', amount: 450, status: 'Due' }, { id: 'INV-1044', customer: 'Grace Hopper', amount: 3100, status: 'Paid' }, { id: 'INV-1045', customer: 'Katherine Johnson', amount: 780, status: 'Overdue' },];
type Sort = 'none' | 'ascending' | 'descending';// Each press moves on: as received, then ascending, then descending, then back.const next: Record<Sort, Sort> = { none: 'ascending', ascending: 'descending', descending: 'none' };
// Sorting is yours: a button in the header, and aria-sort on the header cell.export default function TableBasics() { const [sort, setSort] = useState<Sort>('none'); const rows = sort === 'none' ? invoices : [...invoices].sort((a, b) => sort === 'ascending' ? a.amount - b.amount : b.amount - a.amount, ); const total = invoices.reduce((sum, i) => sum + i.amount, 0);
return ( <Table.Root style={{ minInlineSize: '32rem' }}> <Table.Caption>Invoices this month</Table.Caption> <Table.Header> <Table.Row> <Table.Head>Invoice</Table.Head> <Table.Head>Customer</Table.Head> <Table.Head>Status</Table.Head> {/* No aria-sort while unsorted, so no arrow. */} <Table.Head aria-sort={sort === 'none' ? undefined : sort}> {/* Unstyled: the Head styles the button that fills it. */} <Button onClick={() => setSort(next[sort])}>Amount</Button> </Table.Head> </Table.Row> </Table.Header> <Table.Body> {rows.map((invoice) => ( <Table.Row key={invoice.id}> <Table.Head scope="row">{invoice.id}</Table.Head> <Table.Cell>{invoice.customer}</Table.Cell> <Table.Cell>{invoice.status}</Table.Cell> <Table.Cell>€{invoice.amount.toLocaleString('en')}</Table.Cell> </Table.Row> ))} </Table.Body> <Table.Footer> <Table.Row> <Table.Cell colSpan={3}>Total</Table.Cell> <Table.Cell>€{total.toLocaleString('en')}</Table.Cell> </Table.Row> </Table.Footer> </Table.Root> );}Table is a native <table>, styled. Its parts map onto the table’s elements: Root
(<table>), Caption, Header (<thead>), Body, Footer, Row (<tr>), Head (<th>)
and Cell (<td>). It sits in a container that scrolls sideways when the table is wider than
the page.
Headisscope="col"by default. Usescope="row"for the first cell of a body row, so each cell is read with its row’s name.- Give every table a
Caption. It names the table for screen readers.
Sorting
Section titled “Sorting”Sorting is yours. Put a plain <button> in the column’s Head — @arun-dev/headless’s Button
renders one, unstyled, as the example does — sort your rows when it’s pressed, and
set aria-sort on the Head. The arrow follows aria-sort:
| State | aria-sort | Arrow |
|---|---|---|
| As received, e.g. from your server | leave it off | none |
| Ascending | "ascending" | ↑ |
| Descending | "descending" | ↓ |
The example cycles through all three: each press of Amount moves from the order the rows
arrived in, to ascending, to descending, and back. Keep the original array and sort a copy, so
you can return to it. Only the sorted column carries aria-sort; leave it off the others.
Table.Root
Section titled “Table.Root”| Prop | Type | Default | Description |
|---|---|---|---|
containerClassName | string | Classes for the scrolling container around the table. | |
className | string | Concatenated with the component's own classes, never replacing them. | |
render | ReactElement | Element or component to render instead of the default. Props, className, event handlers and ref are merged onto it. | |
ref | Ref<HTMLElement> | Ref to the rendered element. Merged with any ref on the render element. | |
…rest | HTMLAttributes | Anything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM. |
Table.Head
Section titled “Table.Head”| Prop | Type | Default | Description |
|---|---|---|---|
scope | 'col' | 'row' | 'col' | What the header labels. row for the first cell of a body row. |
The other parts take the props of the element they render.
- This is a table for reading, not a data grid. There is no cell-by-cell keyboard navigation.