On this page
Import
import { Table } from '@envsemble/envui';
Usage
Table's parts are the native table elements, inside a focusable region. Name every Table.Root with a caption, an aria-label or aria-labelledby; the name reaches its scroll region too. Header cells are scope="col"; set scope="row" on one that heads its row, such as Total. Put numeric on a number column's header cell and cells, and nowrap on short values such as names and dates.
import { Badge, Link, Table, Text } from '@envsemble/envui';
const projects = [
{
id: 'website',
name: 'Website refresh',
team: 'Marketing',
owner: 'Alex Morgan',
openTasks: 12,
status: 'In progress',
},
{
id: 'onboarding',
name: 'Onboarding emails',
team: 'Growth',
owner: 'Sam Lee',
openTasks: 4,
status: 'Blocked',
},
{
id: 'billing',
name: 'Billing migration',
team: 'Finance',
owner: 'Priya Shah',
openTasks: 0,
status: 'Done',
},
{
id: 'checkout',
name: 'Mobile checkout',
team: 'Product',
owner: 'Alex Morgan',
openTasks: 27,
status: 'In progress',
},
] as const;
const tones = { 'In progress': 'primary', Blocked: 'warning', Done: 'success' } as const;
export default function ProjectStatus() {
return (
// On a phone, each project is a block of its own rather than a row that scrolls sideways.
<Table.Root caption="Projects" narrowLayout="stack">
<Table.Head>
<Table.Row>
<Table.HeaderCell>Project</Table.HeaderCell>
<Table.HeaderCell>Status</Table.HeaderCell>
<Table.HeaderCell>Owner</Table.HeaderCell>
<Table.HeaderCell numeric>Open tasks</Table.HeaderCell>
</Table.Row>
</Table.Head>
<Table.Body>
{projects.map((project) => (
<Table.Row key={project.id} interactive>
<Table.Cell nowrap>
<Link href={`/projects/${project.id}`} underline={false}>
{project.name}
</Link>
<Text variant="caption">{project.team}</Text>
</Table.Cell>
<Table.Cell label="Status">
<Badge tone={tones[project.status]}>{project.status}</Badge>
</Table.Cell>
<Table.Cell nowrap label="Owner">
{project.owner}
</Table.Cell>
<Table.Cell numeric label="Open tasks">
{project.openTasks}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
}density on Table.Root changes only the cells' padding; controls in cells take the small height.
A Link in a cell, such as the name that opens a row, takes the cell's text color, so a column of names doesn't read as a column of blue; its underline shows under the pointer and with keyboard focus. A secondary line under a cell's text, <Text variant="caption">, keeps to one line and ends in an ellipsis where its column is narrower; give it a title with the full text when that can happen.
Sorting
The application sorts the rows. onSortChange on a header cell makes it a sort button, called with the order pressing asks for, and sortDirection shows the current order on every sortable header:
<Table.HeaderCell
sortDirection={sort.column === 'name' ? sort.direction : 'none'}
onSortChange={(direction) => setSort({ column: 'name', direction })}
>
Name
</Table.HeaderCell>
Selecting and opening rows
selected on a Table.Row reports aria-selected; pass selected={false} to the other rows, and select with a Checkbox labelled with the row's name. interactive only highlights a row that opens through a link or a button in a cell; the row takes no click handler.
Resizable columns
resizableColumns needs widths in percent that add up to 100, in column order: defaultColumnWidths, or columnWidths with onColumnWidthsChange, which receives { committed }, a ResizeDetails, as Panes' and Sheet's width callbacks do: false while people drag and true once a change is complete, the widths to save.
<Table.Root aria-label="Files" resizableColumns defaultColumnWidths={[40, 25, 20, 15]}>
…
</Table.Root>
Sticky header and loading
stickyHeader makes the region scroll vertically, so give it a height through regionClassName or its layout. skeleton shows the table as its own skeleton over placeholder rows until they arrive, without the selection's fill or bar.
Stacked rows
narrowLayout="stack" stacks the body rows below stackBelow, the region's own width. It works by a container query, so server markup already stacks.
- Give every cell after the first a
label, which titles its value in a stacked row. - The first labelled value sits beside the title without its label, so put one there that reads on its own, such as a status.
- A cell without a label, such as the row's menu of actions, joins that line too.
- The other values follow two to a line, each under its label.
- The header row, and its sort buttons, are hidden while rows stack.
onStackedChangetells the application, such as to show a Select for the order instead.
<Table.Root aria-label="Invoices" narrowLayout="stack" onStackedChange={setStacked}>
<Table.Body>
<Table.Row>
<Table.Cell>{invoice.number}</Table.Cell>
<Table.Cell label="Due">{invoice.due}</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
Composition
Parts the anatomy doesn't show: a column's resize handle, env-table-column-resize-handle, and a stacked cell's label, env-table-cell-label.
Accessibility
You provide
- A
captionor anaria-labelfor every table. - The sorting, the selection control and the row actions, named for the row they act on.
- A
labelon every cell after the first in a stacking table.
envUI provides
- Native table elements, with
scope="col"on header cells by default, and a focusable region named like the table. aria-sorton sortable headers, and sort buttons named by the header and what pressing does.- Resize handles as focusable separators with their width in percent.
aria-selectedon rows withselected.- Table roles kept while rows stack, with the header row still read and the stacked labels hidden.
Props
Table.Root
A semantic data table on a rounded surface, in a focusable region that scrolls sideways when the columns do not fit.
| Prop | Type | Description |
|---|---|---|
aria-label | string | Accessible name of the table and its scroll region when there is no caption. A table needs a caption, an aria-label or an aria-labelledby, which names its region too. |
caption | ReactNode | A visible caption above the rows that names the table and its scroll region, in place of aria-label or aria-labelledby. Under a sticky header it scrolls away with the rows. |
densityDefault 'md' | 'xs''sm''md''lg' | How tightly the rows pack: xs, sm, md or lg changes only the cells' block padding, never the text. Buttons, icon buttons and fields in the cells take the small control height in every density, unless they set a size. |
stickyHeaderDefault false | boolean | The header row stays at the top of the region while the rows scroll beneath it. The region then scrolls vertically too, so give it a height through regionClassName or the surrounding layout. |
regionClassName | string | Class of the scroll region around the table, for example to limit its height. |
narrowLayoutDefault 'scroll' | 'scroll''stack' | What the table does when its region is narrower than stackBelow: scroll keeps the columns and scrolls sideways; stack shows each body row as a block that starts with its first cell, followed by the other cells with their labels. The header row is then visually hidden. A container query on the region decides it, so the server's markup and the first paint already stack; a stacking region fills the width its context gives it. |
stackBelowDefault '36rem' | stringnumber | The region width below which a table with narrowLayout="stack" stacks its rows: a CSS length such as 60rem or 480px, or a number of rem, such as 40 or "40", as PanelDock's. Wide tables, such as seven sortable columns, need more. |
onStackedChange | ( | Called with true when the rows stack and with false when the columns return, as the table reads its layout once it is in the document, for example to show another sort control while the header row is hidden; a table that leaves while its rows stack reports false. The region has data-stacked from then on while the rows stack; style stacked rows with a container query on env-table, which applies before hydration, rather than through it. |
columnWidths | readonly number[ | Column widths in percent of the table width, in column order and adding up to 100, for widths the application controls with onColumnWidthsChange. Set widths fix the columns: long words wrap and nowrap cells end with an ellipsis. |
defaultColumnWidths | readonly number[ | Starting column widths, in percent, when the table owns them; other defaults start them over. Double-clicking a resize handle restores them, or else the widths a controlled table started with. |
onColumnWidthsChange | ( | Called with every column's width when people resize a column: while dragging a handle with committed false, and with committed true as a drag ends, a key moves it or a double click resets the widths. |
minColumnWidthsDefault 5 | numberreadonly number[ | Smallest width of each column in percent, in column order, or one value for every column. |
resizableColumnsDefault false | boolean | People can resize the columns with a handle at the end of each header cell but the last, which moves the boundary to the next column and keeps the table's width. Needs columnWidths or defaultColumnWidths. |
skeletonDefault false | boolean | Shows it and its content as a busy skeleton while that loads; turning it on or off renders the content anew. |
Also accepts the native props of <table>, such as ref, className and aria-* attributes.
Table.Head
The header rows of a table, a native thead, which hold the column headers.
No props of its own.
Also accepts the native props of <thead>, such as ref, className and aria-* attributes.
Table.Body
The data rows of a table, a native tbody, separated by hairlines.
No props of its own.
Also accepts the native props of <tbody>, such as ref, className and aria-* attributes.
Table.Row
A row of a table, a native tr, which can highlight on hover or be selected.
| Prop | Type | Description |
|---|---|---|
interactiveDefault false | boolean | The row highlights on hover and while focus is inside it, for rows that open something through a link or button in a cell. It adds no behavior of its own. |
selected | boolean | The row is selected: it reports aria-selected and takes the selection colors, with a bar at its start. Pass false to the other rows of a table whose rows can be selected; unset, a row reports no selection state. |
Also accepts the native props of <tr>, such as ref, className and aria-* attributes.
Table.HeaderCell
A header cell, a native th that heads its column, or its row with scope="row". With onSortChange it sorts its column; in a table with resizable columns it holds the column's resize handle.
| Prop | Type | Description |
|---|---|---|
sortDirectionDefault 'none' with onSortChange | 'ascending''descending''none' | The column's sort order: aria-sort on the header and the arrow in its sort button. Use it with onSortChange; without it only sets aria-sort. |
onSortChange | ( | The header becomes a button across the cell that asks the application to sort the rows by this column, with the order pressing it asks for: descending while the column is sorted ascending, ascending otherwise. The application sorts the rows and sets sortDirection. |
sortLabelDefault '{header} sort {next order}', from EnvUIProvider's messages.table.sortAscending and sortDescending | string | Accessible name of the sort button, which says what pressing it does: the header's name followed by "sort ascending", or by "sort descending" while the column is sorted ascending. |
resizeLabelDefault 'Resize {header}', from EnvUIProvider's messages.table.resize | string | Accessible name of the column's resize handle, "Resize" followed by the header's name. |
alignDefault 'start', or 'end' with numeric | 'start''end' | Aligns the content with the start or the end of the cell. |
numericDefault false | boolean | Tabular figures aligned to the end, for numbers that compare down a column. In a sortable header the arrow comes before the label, so the label lines up with the numbers. |
nowrapDefault false | boolean | Keeps the content on one line. In a table with set column widths, content that doesn't fit ends with an ellipsis. |
Also accepts the native props of <th>, such as ref, className and aria-* attributes.
Table.Cell
A data cell, a native td, with a label that names its value while rows stack.
| Prop | Type | Description |
|---|---|---|
label | ReactNode | The column's name, shown before the value while a table with narrowLayout="stack" stacks its rows. Assistive technology reads the column header instead. |
alignDefault 'start', or 'end' with numeric | 'start''end' | Aligns the content with the start or the end of the cell. |
numericDefault false | boolean | Tabular figures aligned to the end, for numbers that compare down a column. In a sortable header the arrow comes before the label, so the label lines up with the numbers. |
nowrapDefault false | boolean | Keeps the content on one line. In a table with set column widths, content that doesn't fit ends with an ellipsis. |
Also accepts the native props of <td>, such as ref, className and aria-* attributes.
Part names
Each part's class and state attributes are public and versioned, for an application's CSS to style.
env-table- The table.
env-table-body- The body.
env-table-caption- The caption.
env-table-cell- A cell.
env-table-cell-content- A cell's content.
env-table-cell-label- A cell's column name, shown when rows stack.
env-table-column-resize-handle- The handle that resizes a column.
env-table-head- The head.
env-table-header-cell- A column header.
env-table-header-label- A column header's text.
env-table-region- The region that scrolls the table.
env-table-row- A row.
env-table-sort-button- The button that sorts by a column.
data-alignend·starton env-table-cell, env-table-header-celldata-densitylg·md·sm·xsdata-fixed-layout- Present
data-interactive- Present on env-table-row
data-labelled- Present on env-table-cell
data-narrow-layoutstackon env-table-regiondata-nowrap- Present on env-table-cell, env-table-header-cell
data-numeric- Present on env-table-cell, env-table-header-cell
data-resizable- Present on env-table-header-cell
data-resizing- Present on env-table, env-table-column-resize-handle
data-sortable- Present on env-table-header-cell
data-stacked- Present on env-table-region
data-sticky-header- Present on env-table-region
Strings
EnvUIProvider's messages overrides each, and a component's own label prop wins over it.
messages.table.sortAscending"sort ascending"Read after a sortable column's name while pressing sorts it ascending.messages.table.sortDescending"sort descending"Read after a sortable column's name while pressing sorts it descending.messages.table.resize"Resize"A column's resize handle.
Changelog
- 1.0.0
The parts are members of the
Tablenamespace:TableisTable.Root, andTableHead,TableBody,TableRow,TableHeaderCellandTableCellareTable.Head,Table.Body,Table.Row,Table.HeaderCellandTable.Cell;TableSortDirectionisTable.SortDirection. The page istable, after the component. Table.HeaderCell'sonSortisonSortChange, called with the order pressing asks for. A table named byaria-labelledbynames its scroll region too, which was left unnamed. The sort and resize words come frommessages.table. A column's resize handle,env-table-column-resize, isenv-table-column-resize-handle, as every resize handle is named.- 1.0.0-prerelease.6
Colors under the pointer and while pressed change with
--env-motion-ease-enter, rather than easing in slowly, so hover and press show from the first frame.A selected row takes a lighter tint of the selection fill, still a step above a row under the pointer; at high contrast it keeps the full fill.
A sortable header's focus ring has a row's rounded corners. In dark, the header band and row dividers are a light step closer to the surface. A stacked row's labels sit directly over their values.
A sortable header under the pointer takes no fill: its label darkens to the text color, with its arrow showing.
A sortable header's focus ring sits a hair inside the header, so the frame's rounded corners never cut it in the first and last headers.
An unsorted sortable header shows its arrow only while the pointer is over the header row or the header has keyboard focus, where the pointer hovers; the sorted column's arrow always shows.
A stacked row is a compact block: the first labelled value, such as a status, sits beside the title without its label, an unlabelled cell such as the row's menu joins them, and the other values follow two to a line, each under its label, in place of a line for each value. The row densities differ by at least 4 px of row height in the compact scale:
xsandsmrows are shorter, andmdkeeps its height.A skeleton shows no selection. A row under the pointer takes
--env-color-bg-row-hover, clear in light and dark and a step below the selection.A selected row whose first cell holds a checkbox takes no bar at its start, as the checkbox and the fill say it already; a row selected without one keeps the bar.
Links and times repeat down the rows, so a Link in a table takes the cell's text color and shows its underline only under the pointer and with keyboard focus, and a RelativeTime its dotted line; in a selected row too. A secondary line keeps to one line, ending in an ellipsis. A skeleton's sortable headers are bars, as the others are.
- 1.0.0-prerelease.5
A number for
stackBelowis a number of rem, as on PanelDock, where it was pixels.onColumnWidthsChangereceives{ committed }beside the widths, as the other resize callbacks do.