Basic
| Patient | Service | Time |
|---|---|---|
| Ana Torres | General check-up | 09:30 |
| Luis Gómez | Dental cleaning | 10:15 |
| Marta Ruiz | Vaccination | 11:00 |
import { Cell, Column, Row, Table, TableBody, TableHeader } from '@arbol-ai/ui';
const APPOINTMENTS = [
{
id: 'a1',
patient: 'Ana Torres',
document: 'CC 1.020.304.050',
service: 'General check-up',
time: '09:30',
minutes: 30,
},
{
id: 'a2',
patient: 'Luis Gómez',
document: 'CC 80.111.222',
service: 'Dental cleaning',
time: '10:15',
minutes: 45,
},
{
id: 'a3',
patient: 'Marta Ruiz',
document: 'TI 1.001.234.567',
service: 'Vaccination',
time: '11:00',
minutes: 15,
},
];
<Table aria-label="Upcoming appointments">
<TableHeader>
<Column isRowHeader>Patient</Column>
<Column>Service</Column>
<Column align="end">Time</Column>
</TableHeader>
<TableBody>
{APPOINTMENTS.map((appointment) => (
<Row id={appointment.id} key={appointment.id}>
<Cell>{appointment.patient}</Cell>
<Cell>{appointment.service}</Cell>
<Cell align="end">{appointment.time}</Cell>
</Row>
))}
</TableBody>
</Table>Rows that open their record
A press anywhere on a row opens the patient; the name is a real link (new tab, copy link), and the “⋯” menu holds the rest. Arrow keys reach the link and the menu.
| Patient | Service | Time | Actions |
|---|---|---|---|
CC 1.020.304.050 | General check-up | 09:30 | |
Luis GómezLong visit CC 80.111.222 | Dental cleaning | 10:15 | |
TI 1.001.234.567 | Vaccination | 11:00 |
import { Avatar, Badge, Cell, Column, Row, RowMenu, RowTitle, Table, TableBody, TableHeader } from '@arbol-ai/ui';
const APPOINTMENTS = [
{
id: 'a1',
patient: 'Ana Torres',
document: 'CC 1.020.304.050',
service: 'General check-up',
time: '09:30',
minutes: 30,
},
{
id: 'a2',
patient: 'Luis Gómez',
document: 'CC 80.111.222',
service: 'Dental cleaning',
time: '10:15',
minutes: 45,
},
{
id: 'a3',
patient: 'Marta Ruiz',
document: 'TI 1.001.234.567',
service: 'Vaccination',
time: '11:00',
minutes: 15,
},
];
<Table aria-label="Patients">
<TableHeader>
<Column isRowHeader>Patient</Column>
<Column>Service</Column>
<Column align="end">Time</Column>
<Column className="w-12">
<span className="sr-only">Actions</span>
</Column>
</TableHeader>
<TableBody>
{APPOINTMENTS.map((appointment) => (
<Row href={`#/patients/${appointment.id}`} id={appointment.id} key={appointment.id}>
<Cell>
<RowTitle
badge={appointment.minutes > 30 ? <Badge tone="info">Long visit</Badge> : undefined}
description={appointment.document}
href={`#/patients/${appointment.id}`}
leading={<Avatar name={appointment.patient} />}
>
{appointment.patient}
</RowTitle>
</Cell>
<Cell>{appointment.service}</Cell>
<Cell align="end">{appointment.time}</Cell>
<Cell>
<RowMenu
actions={[
{ label: 'Book again', href: `#/book?patient=${appointment.id}` },
{ label: 'Archive', isDestructive: true, onAction: () => undefined },
]}
label={`Actions for ${appointment.patient}`}
/>
</Cell>
</Row>
))}
</TableBody>
</Table>While the rows load
Placeholder rows hold the table’s place under its real header, across every column.
| Patient | Service | Time |
|---|---|---|
import { Column, Table, TableBody, TableHeader } from '@arbol-ai/ui';
<Table aria-label="Upcoming appointments">
<TableHeader>
<Column isRowHeader>Patient</Column>
<Column>Service</Column>
<Column align="end">Time</Column>
</TableHeader>
<TableBody isLoading>{[]}</TableBody>
</Table>Sorting and row actions
Press a header to sort by it; press a row (or Enter on a focused one) to open it.
| Patient | Service | Minutes |
|---|---|---|
| Ana Torres | General check-up | 30 |
| Luis Gómez | Dental cleaning | 45 |
| Marta Ruiz | Vaccination | 15 |
Opened: —
import { useState } from 'react';
import type { SortDescriptor } from 'react-aria-components';
import { Cell, Column, Row, Table, TableBody, TableHeader } from '@arbol-ai/ui';
const APPOINTMENTS = [
{
id: 'a1',
patient: 'Ana Torres',
document: 'CC 1.020.304.050',
service: 'General check-up',
time: '09:30',
minutes: 30,
},
{
id: 'a2',
patient: 'Luis Gómez',
document: 'CC 80.111.222',
service: 'Dental cleaning',
time: '10:15',
minutes: 45,
},
{
id: 'a3',
patient: 'Marta Ruiz',
document: 'TI 1.001.234.567',
service: 'Vaccination',
time: '11:00',
minutes: 15,
},
];
const SortableExample = () => {
const [sort, setSort] = useState<SortDescriptor>({ column: 'patient', direction: 'ascending' });
const [opened, setOpened] = useState('');
const rows = APPOINTMENTS.toSorted((a, b) => {
const order = sort.column === 'minutes' ? a.minutes - b.minutes : a.patient.localeCompare(b.patient);
return sort.direction === 'ascending' ? order : -order;
});
return (
<div className="flex flex-col gap-2">
<Table
aria-label="Appointments"
onRowAction={(id) => {
setOpened(String(id));
}}
onSortChange={setSort}
sortDescriptor={sort}
>
<TableHeader>
<Column allowsSorting id="patient" isRowHeader>
Patient
</Column>
<Column id="service">Service</Column>
<Column align="end" allowsSorting id="minutes">
Minutes
</Column>
</TableHeader>
<TableBody items={rows}>
{(appointment) => (
<Row id={appointment.id}>
<Cell>{appointment.patient}</Cell>
<Cell>{appointment.service}</Cell>
<Cell align="end">{appointment.minutes}</Cell>
</Row>
)}
</TableBody>
</Table>
<p className="text-muted-foreground text-sm">Opened: {opened === '' ? '—' : opened}</p>
</div>
);
};
<SortableExample />Selection
selectionMode adds a checkbox per row and, for multiple, a select-all box.
| Patient | Service | |
|---|---|---|
| Ana Torres | General check-up | |
| Luis Gómez | Dental cleaning | |
| Marta Ruiz | Vaccination |
1 selected
import { useState } from 'react';
import type { Selection } from 'react-aria-components';
import { Cell, Column, Row, Table, TableBody, TableHeader } from '@arbol-ai/ui';
const APPOINTMENTS = [
{
id: 'a1',
patient: 'Ana Torres',
document: 'CC 1.020.304.050',
service: 'General check-up',
time: '09:30',
minutes: 30,
},
{
id: 'a2',
patient: 'Luis Gómez',
document: 'CC 80.111.222',
service: 'Dental cleaning',
time: '10:15',
minutes: 45,
},
{
id: 'a3',
patient: 'Marta Ruiz',
document: 'TI 1.001.234.567',
service: 'Vaccination',
time: '11:00',
minutes: 15,
},
];
const SelectionExample = () => {
const [selected, setSelected] = useState<Selection>(new Set(['a2']));
const count = selected === 'all' ? APPOINTMENTS.length : selected.size;
return (
<div className="flex flex-col gap-2">
<Table
aria-label="Appointments to confirm"
onSelectionChange={setSelected}
selectedKeys={selected}
selectionMode="multiple"
>
<TableHeader>
<Column isRowHeader>Patient</Column>
<Column>Service</Column>
</TableHeader>
<TableBody items={APPOINTMENTS}>
{(appointment) => (
<Row id={appointment.id}>
<Cell>{appointment.patient}</Cell>
<Cell>{appointment.service}</Cell>
</Row>
)}
</TableBody>
</Table>
<p className="text-muted-foreground text-sm">{count} selected</p>
</div>
);
};
<SelectionExample />Empty
| Campaign | Status |
|---|---|
No campaigns yet | |
import { Column, EmptyState, Table, TableBody, TableHeader } from '@arbol-ai/ui';
<Table aria-label="Campaigns">
<TableHeader>
<Column isRowHeader>Campaign</Column>
<Column>Status</Column>
</TableHeader>
<TableBody renderEmptyState={() => <EmptyState title="No campaigns yet" />}>{[]}</TableBody>
</Table>Source
The examples above are src/components/table/table.examples.tsx, which the library’s tests render too.