DataTable
DataTable adds operational behavior to a set of records. Choose it when people need to sort, select or act on rows; choose Table when the records only need a simple, comparable tabular presentation.
Examples
Operational records
Start with stable column keys and row id values. Put a named native action in its own cell;
hover remains an optional scanning affordance.
| Project | Owner | Status | Actions |
|---|---|---|---|
| Atlas | Maya Chen | Healthy | |
| Orbit | Jon Bell | At risk | |
| Nova | Priya Shah | Planning |
Choose a project action to view its details.
Sorting and row selection
Mark only the fields that support a useful order as sortable, and provide labels.selectRow as
a function when each checkbox needs to announce the person or record it selects.
| Maya Chen | Design | 4 | |
|---|---|---|---|
| Jon Bell | Engineering | 12 | |
| Priya Shah | Operations | 7 |
Loading rows
Pass a number to loading when the expected result has a known density. It keeps the table's
shape while records are being fetched instead of replacing the data region with a spinner.
| Project | Owner | Updated | |
|---|---|---|---|
When to use
Use DataTable for operational record sets that people need to sort, select, scan in a fixed area or manage with explicit actions.
Reach for something else when:
- The records only need aligned fields for comparison — use Table, the simpler static table.
- The records are files people need to browse and act on — use FileManager.
- There are no records to show after loading — use EmptyState for a full-page absence, or
DataTable's
emptycontent when the table itself remains useful context.
Accessibility
-
DataTable renders a real
<table>with table headings and cells. A sortable heading is a button, and the active heading reportsaria-sortas ascending or descending. -
With
selectable, it renders native checkboxes for each row and a select-all checkbox. The select-all checkbox becomes indeterminate when only part of the set is selected. -
labels.selectRowdefaults to the repeated English name "Select row". Pass a string or a function such as(row) => 'Select ' + row.nameto give every row checkbox a translated, distinguishable accessible name. -
A row's
idis used for its React key and selection. If a row has no string or numericid, its fallback comes from its original input position, so selection stays with that row after sorting. -
DataTable owns table layout, sorting, and selection. A command belongs to the React consumer in a cell: use a native button for an operation and a native link for navigation. Plain cells and rows are not commands.
-
Set
hovertotrueonly when a visual scanning affordance is useful. It is not implied by a consumer action. -
Give every table a descriptive
caption(usecaptionHiddenwhen it should only be read by assistive technology).aria-labelandaria-labelledbyare forwarded to the table. -
Mark the identifying column with
rowHeader: true; it renders body cells as<th scope="row">. Column headings usescope="col". -
The scroll area is a focusable, named region. Its name comes from the caption, or you can set
scrollLabel. While loading, the table exposesaria-busyandlabels.loadingsupplies a hidden status announcement.
React cell actions
Move each former row command into an explicit action cell during manual migration. The old
onRowClick contract is removed under the project's pre-1.0 compatibility exception for unsafe
contracts; the earliest correction is 0.6.0 before stable release, or stable 1.0.0. Keep the
action's stable domain identity in the consumer, and keep selection and sorting callbacks separate.
Styles and the static Alpine contract are unchanged.
import { useState } from 'react';
import { Badge, Button, DataTable, type DataTableColumn } from '@lyra-ds/react';
const columns: DataTableColumn[] = [
{ key: 'project', label: 'Project', rowHeader: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'actions', label: 'Actions' },
];
const projects = [
{
id: 'atlas',
project: 'Atlas',
owner: 'Maya Chen',
status: <Badge tone="success">Healthy</Badge>,
},
{
id: 'orbit',
project: 'Orbit',
owner: 'Jon Bell',
status: <Badge tone="warning">At risk</Badge>,
},
{
id: 'nova',
project: 'Nova',
owner: 'Priya Shah',
status: <Badge tone="neutral">Planning</Badge>,
},
];
export function ProjectTable() {
const [openedProject, setOpenedProject] = useState<string | null>(null);
const rows = projects.map((project) => ({
...project,
actions: (
<Button
type="button"
size="sm"
variant="secondary"
onClick={() => setOpenedProject(project.id)}
tabIndex={0}
>
Open {project.project}
</Button>
),
}));
const opened = projects.find((project) => project.id === openedProject);
return (
<div>
<DataTable caption="Projects" columns={columns} rows={rows} hover />
<p aria-live="polite">
{opened
? `Showing details for ${opened.project}. Owner: ${opened.owner}.`
: 'Choose a project action to view its details.'}
</p>
</div>
);
}API and code
| Name | Type | Required | Description |
|---|---|---|---|
columns | DataTableColumn[] | Required | Columns rendered in the supplied order. |
rows | RowShape[] | Required | Row records whose values may be any renderable React node. An `id` value is used for keys and selection. |
caption | ReactNode | — | Table caption. Supply a meaningful name for each data set. |
captionHidden | boolean | — | Visually hide the caption while keeping it available to assistive technology. |
scrollLabel | string | — | Accessible name for the focusable scroll region when it differs from the table name. |
sorting | DataTableSorting | null | — | Controlled sorting state. Pass `null` for unsorted rows. |
defaultSorting | DataTableSorting | null | — | Initial sorting state when uncontrolled. Default: `null`. |
onSortChange | (sorting: DataTableSorting | null) => void | — | Called after sorting changes. |
selectable | boolean | — | Whether to render row and select-all checkboxes. |
selected | Array<string | number> | — | Controlled selected row identifiers. |
defaultSelected | Array<string | number> | — | Initial selected row identifiers when uncontrolled. |
onSelectionChange | (selected: Array<string | number>) => void | — | Called after the selected row identifiers change. |
stickyHeader | boolean | — | Whether the header remains visible while the table scrolls. |
maxHeight | number | string | — | Maximum height for the scrollable table area. |
density | 'comfortable' | 'compact' | — | Row density. Default: `"comfortable"`. |
loading | boolean | number | — | Whether to render loading placeholders, or the number of placeholder rows. |
empty | ReactNode | — | Content shown instead of the default empty-state label. |
footer | ReactNode | — | Content rendered below the scrollable table area. |
hover | boolean | — | Whether rows highlight on hover. |
labels | DataTableLabels | — | Labels for controls and the default empty state. Merged over the English defaults. |
x-data="lyraDataTable({ … })"
| Option | Type | Required | Description |
|---|---|---|---|
sorting | LyraDataTableSorting | null | — | Controlled sort state. Modelable with `x-modelable="sorting"`. Default: `null`. |
selected | string[] | — | Controlled selected served row ids. Modelable with `x-modelable="selected"`. Default: `[]`. |
clientSort | boolean | — | Reorder served rows in the browser after sorting. Default: `false`. |
scrollLabel | string | — | Accessible name for the focusable scroll region; defaults to the table caption or label. |
For the Alpine binding, serve a <caption> and <th scope="row"> for each identifying cell. Add x-bind="header" to sortable column <th> elements, x-bind="rowHeader" to row <th> elements, and x-bind="scrollRegion" to .lyra-table-scroll; use lyraDataTable({ scrollLabel: "…" }) to override its name. Non-sortable column headers need scope="col" in served HTML.
Without React, compose the table classes — you own sorting, selected-row state, checkbox labels and the loading transition:
<div class="lyra-table-wrap">
<div class="lyra-table-scroll" role="region" tabindex="0" aria-label="Projects">
<table class="lyra-table lyra-table--hover">
<caption>
Projects
</caption>
<thead>
<tr>
<th scope="col" class="lyra-table__check">
<input class="lyra-checkbox" type="checkbox" aria-label="Select all" />
</th>
<th scope="col" aria-sort="ascending">
<button class="lyra-table__sortbtn lyra-table__sortbtn--active" type="button">
Project
</button>
</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
<tr class="lyra-table__row--selected">
<td class="lyra-table__check">
<input class="lyra-checkbox" type="checkbox" aria-label="Select Atlas" checked />
</td>
<th scope="row" class="lyra-table__primary">Atlas</th>
<td>Healthy</td>
</tr>
</tbody>
</table>
</div>
</div><lyra:data-table> Generated from lyra-ds/blade v0.10.0.
The behavior comes from lyraDataTable() — install @lyra-ds/alpine and see the HTML + Alpine tab.
| Prop | Default | Required | Example values |
|---|---|---|---|
columns | — | Required | — |
rows | — | Required | — |
sorting | null | — | — |
selectable | false | — | — |
selected | [] | — | — |
clientSort | false | — | — |
stickyHeader | false | — | — |
maxHeight | null | — | — |
density | 'comfortable' | — | compact |
loading | false | — | — |
empty | null | — | — |
hover | false | — | — |
labels | [] | — | — |
<lyra:data-table
:columns="[
['key' => 'name', 'label' => 'Project', 'sortable' => true],
['key' => 'owner', 'label' => 'Owner'],
['key' => 'issues', 'label' => 'Open issues', 'align' => 'end'],
]"
:rows="[
['id' => '1', 'name' => 'Website redesign', 'owner' => 'Ana Ribeiro', 'issues' => 12],
['id' => '2', 'name' => 'Mobile app', 'owner' => 'João Martins', 'issues' => 4],
]"
density="comfortable"
hover
/>