Data table
A bordered table with sortable columns and a scroll cap, for small sets of rows that are already loaded.
import { DataTable } from "@/ui";DataTable takes a list of columns and a list of row objects and renders a table. It sorts on the client, scrolls sideways when the columns do not fit, and scrolls down once there are more than 10 rows. There is no pagination, filtering, selection or row click.
No screen uses DataTable yet. The tables in chat sessions come from DataTableViewer, a separate component.
Style clash. src/index.css has global .data-table th and .data-table td rules, written for older session tables, and DataTable's root also has the class data-table. Those global rules are more specific than DataTable's own CSS, so they win everywhere in the app, including this page. What you see below is what ships today: uppercase 12px headers on --bg-secondary (#EEF0F7), 12px JetBrains Mono cells with 6px 10px padding, and cells capped at 240px with an ellipsis. DataTable.css intends 12px semibold Poppins headers in --color-text-secondary on --color-grey-50, and 14px Poppins cells with 12px 16px padding. Renaming the root class would fix it.
Basic table
Each column has a key that matches a field on the rows and a label for the header. Click a sortable header to sort ascending, again for descending.
const columns = [
{ key: "campaign", label: "Campaign", sortable: true, width: "200px" },
{ key: "channel", label: "Channel", sortable: true },
{ key: "spend", label: "Spend ($)", sortable: true },
{ key: "leads", label: "Leads", sortable: true },
{ key: "pipeline", label: "Pipeline ($)", sortable: true },
];
const rows = [
{ id: 1, campaign: "Q3 webinar series", channel: "Events", spend: 18400, leads: 96, pipeline: 412000 },
{ id: 2, campaign: "Brand search", channel: "Paid search", spend: 22150, leads: 141, pipeline: 268000 },
// …
{ id: 5, campaign: "Competitor comparison page", channel: "Organic", spend: null, leads: 52, pipeline: 140000 },
];
<DataTable columns={columns} rows={rows} />Missing values (null or undefined) show as a dash and always sort to the bottom. Rows use row.id as their React key, or their index if there is no id, so give rows an id.
How sorting works
| Values in the column | Compared as |
|---|---|
| Both numbers | Numbers |
| Anything else | Lower-cased strings |
Sorting uses the raw value in the row, and the cell shows that same raw value. There is no separate formatter. So you choose between numbers that sort correctly but show as 412000, or formatted strings such as "$412K" that show well but sort as text ("$87K" comes after "$412K"). The examples put the unit in the header and keep numbers raw.
A click on the sorted column flips the direction. There is no way to clear the sort except by reloading the component.
Zebra rows
zebra gives every second row a --color-grey-50 background. On a white table it is a very light stripe.
<DataTable columns={columns} rows={rows} zebra />Components in cells
A cell can hold any React node, such as a Tag. Do not make that column sortable: sorting would compare the nodes as the text [object Object].
const columns = [
{ key: "page", label: "Landing page" },
{ key: "status", label: "Status" },
{ key: "visits", label: "Visits", sortable: true },
{ key: "conversion", label: "Conversion (%)", sortable: true },
];
const rows = [
{ id: "a", page: "/q3-webinar", status: <Tag color="success-green">Published</Tag>, visits: 4820, conversion: 6.1 },
{ id: "b", page: "/pipeline-report", status: <Tag color="warning-yellow">Unpublished changes</Tag>, visits: 2310, conversion: 4.4 },
{ id: "c", page: "/abm-demo", status: <Tag color="column">Draft</Tag>, visits: null, conversion: null },
];Long tables
With more than 10 rows the body gets a 480px maximum height and scrolls, and the header stays fixed at the top (stickyHeader, on by default).
<DataTable columns={columns} rows={sixteenWeeksOfNurtureEmails} />The 480px cap was sized for 10 rows of 44px. With the clash above, rows are about 35px, so about 12 fit. With DataTable.css alone, rows would be about 47px and about 9 would fit. Either way it is not exactly 10.
Empty state
With no rows, the table keeps its header and shows emptyMessage in a single centred cell. DataTable.css gives that cell 48px of vertical padding, but the global td rule cuts it to 6px, so the empty state is a thin row.
<DataTable
columns={columns}
rows={[]}
emptyMessage="No campaigns match these filters. Try a wider date range."
/>Styles
| Part | Value |
|---|---|
| Frame | 1px --color-grey-200 border, 8px radius (--radius-md), white background |
| Row hover | --color-primary-50 on the row, with --bg-hover (#EDF2FE) on the cells from the global rule |
| Zebra row | --color-grey-50 |
| Sort icons | 10px bold carets, --color-grey-300, the active one --color-primary-500 |
| Row divider | 1px --border-primary (#D4D9EA) from the global rule; DataTable.css intends --color-grey-100 |
DataTable.css colours the sorted column's header --color-primary-500, but the global header rule overrides that too, so only the caret turns blue.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
columns | { key, label, sortable?, width? }[] | [] | width is applied as the column's min-width. |
rows | object[] | [] | Fields are read by column key. Give each row an id. |
zebra | boolean | false | |
stickyHeader | boolean | true | Only matters once the table scrolls. |
emptyMessage | string | "No data available" | |
className | string | On the outer wrapper. |
Any other prop (aria-label, id, data-*) goes to the outer <div>, not to the <table>. The source comment mentions pagination; there is none.
Accessibility
- It renders a real
<table>with<thead>,<th>and<td>, so screen readers can move by row and column. - The sorted column gets
aria-sort="ascending"or"descending". Other sortable columns get noaria-sort. - Sortable headers are plain
<th>elements with a click handler. They cannot be focused or used with the keyboard, so keyboard users cannot sort. Sorting should move into a<button>inside each header. - There is no caption, and
aria-labellands on the wrapper<div>. Put a visible heading right above the table that says what it lists.
Do and don't
- Do use DataTable for up to a few hundred rows already in memory, such as campaign results for one quarter.
- Do keep numbers raw in sortable columns and put the unit in the header.
- Do write an
emptyMessagethat says why it is empty and what to try. - Don't mark columns that hold components or formatted strings as sortable.
- Don't use it for large or server-paged data. It has no pagination.
- Don't add the class
data-tableto other elements. It already causes the clash described above.