Table
Rows and columns, built out of the elements that are already accessible.
| Accession | Patient | Study | Received | Status |
|---|---|---|---|---|
| ACC-482913 | Kaur R. | CT chest | 09:12 | Unread |
| ACC-482910 | Prasad M. | Chest X-ray | 09:04 | In review |
| ACC-482904 | Fernandes J. | CT head | 08:51 | Preliminary |
| ACC-482891 | Iyer S. | Chest X-ray | 08:33 | Signed |
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/table.jsonUsage
import {
Table, TableBody, TableCaption, TableCell, TableFooter,
TableHead, TableHeader, TableRow,
} from '@/components/ui/table'<Table>
<TableHeader>
<TableRow>
<TableHead>Accession</TableHead>
<TableHead>Study</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableHead scope="row" numeric>ACC-482913</TableHead>
<TableCell>CT chest</TableCell>
</TableRow>
</TableBody>
</Table>There is no primitive under this and there does not need to be. <table> is already the
accessible thing: a screen reader announces the column header before every cell beneath it, and
tracks which row you are in as you move. Nothing built out of divs gets that back without a great
deal of ARIA, carefully maintained.
This is a table, not a data grid. If you need sorting, column resizing, virtual rows or keyboard cell navigation, that is a grid — and it wants TanStack Table underneath these elements, not a different set of elements.
Composition
<Table> {/* owns density */}
<TableCaption> {/* the accessible name — first in the markup */}
<TableHeader> {/* thead; `sticky` pins it */}
<TableRow><TableHead> {/* th scope="col" */}
<TableBody>
<TableRow><TableHead scope="row"> {/* the cell that names the row */}
<TableCell> {/* td */}
<TableFooter> {/* tfoot — totals */}
</Table>The one piece worth learning is scope="row". The cell that identifies a row — an accession
number, a patient — should be a TableHead with scope="row", not a TableCell. Do that and a
screen reader reading the Status column says "ACC-482913, Status, Unread" rather than just
"Unread", which is the difference between a table you can navigate and a table you have to count
your way across.
Row identity and totals
| Series | Slices | CTDIvol (mGy) | DLP (mGy·cm) |
|---|---|---|---|
| Scout | 2 | 0.4 | 12.6 |
| Non-contrast axial | 312 | 6.8 | 241.3 |
| Arterial phase | 298 | 7.1 | 258.9 |
| Delayed | 144 | 3.2 | 96.4 |
| Total | 756 | — | 609.2 |
TableCaption is the table's accessible name and is read before any cell, so it earns its place
whenever the table is one of several on a page. Put it first in the markup even though it renders
above the header — that is where the element belongs, and the browser positions it.
numeric switches the column to tabular figures so digits line up. Alignment is a separate prop
on purpose: an accession number is an identifier and reads from the left, a dose is a quantity and
reads from the right.
Density
| Accession | Study | Reader | |
|---|---|---|---|
| ACC-482913 | CT chest | A. Mehta | |
| ACC-482910 | Chest X-ray | S. Iyer | |
| ACC-482904 | CT head | P. Nair | |
| ACC-482891 | Chest X-ray | R. Bhatt |
1 of 4 selected
density="compact" tightens the row height for a worklist, where fitting one more study on screen
matters more than air. Leave the default for a report or a summary table that is read rather than
scanned.
The same example shows selection. selected on a row is styling only — a plain table has no
ARIA selection state — so the checkbox carries the meaning and the tint reinforces it. A row that
is only tinted is a row that is not selected as far as assistive technology is concerned.
Sticky header
Table renders no scroll container of its own, which is deliberate. Wrapping the table in an
overflow div quietly makes that div the containing block for position: sticky, and the header
then sticks to a box that is not scrolling — the classic "my sticky thead sits still" bug.
Wrap it in ScrollArea instead and pass sticky to the header. The
scroll area's viewport is the thing that moves, so the header resolves against it.
<ScrollArea className="h-72">
<Table density="compact">
<TableHeader sticky>…</TableHeader>With other components
| Accession | Study | Reader | Status |
|---|---|---|---|
| ACC-482913 | CT chest | AMA. Mehta | Critical |
| ACC-482910 | Chest X-ray | SIS. Iyer | Past SLA |
| ACC-482904 | CT head | PNP. Nair | In review |
| ACC-482891 | Chest X-ray | RBR. Bhatt | Signed |
Status uses a soft Badge rather than a solid one. The design system is explicit about this: tertiary backgrounds for inline status on a white row, so a whole column of them reads as a table rather than as a colour chart.
API Reference
Table
| Prop | Type | Default | Description |
|---|---|---|---|
density | "default" | "compact" | "default" | compact tightens row height for a worklist. |
TableHeader
| Prop | Type | Default | Description |
|---|---|---|---|
sticky | boolean | false | Pins the header while the surrounding scroll container moves. |
TableRow
| Prop | Type | Default | Description |
|---|---|---|---|
selected | boolean | false | Tints the row. Styling only — pair it with a checkbox. |
TableHead and TableCell
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | "start" | Text alignment within the cell. |
numeric | boolean | false | Tabular figures, so digits line up down the column. |
scope | "col" | "row" | "col" | TableHead only. Use row on the cell that names its row. |
TableCaption, TableBody and TableFooter render <caption>, <tbody> and <tfoot> and take
the usual props for those elements.