Table
A tabular data display built on the native <table> element, with column headers that become sort links and a trailing row that becomes a "load more" link.
| Invoice | Customer | Issued | Status | Total | |
|---|---|---|---|---|---|
| INV-2041 | Northwind Traders | 12 Sep | paid | $1,280.00 | |
| INV-2040 | Acme Design | 11 Sep | open | $480.00 | |
| INV-2039 | Globex | 9 Sep | overdue | $3,900.00 | |
| INV-2038 | Initech | 4 Sep | paid | $96.00 | |
| INV-2037 | Hooli | 1 Sep | paid | $740.00 | |
| Load the next 25 invoices | |||||
View code
let selection = new SelectionModel({ mode: "multiple", keys, selectedKeys: ["INV-2039"] });
selection.addEventListener("change", () => void handle.update());
// The headers link, so the order is whatever the query asks for. An app would serve
// the page back sorted; a page that is its own server writes the query and re-reads it.
let sort = readSort(globalThis.location?.search ?? "");
function sortFromHeader(event) {
let link = event.target.closest('th[aria-sort] a');
if (!link) return;
event.preventDefault();
globalThis.history.pushState(null, "", link.href);
sort = readSort(globalThis.location.search);
void handle.update();
}
// Only the active column carries a direction; the rest show the idle indicator.
let direction = (key) => (sort.key === key ? sort.direction : undefined);
<div mix={[on("click", sortFromHeader)]}>
<Table.Container>
<Table aria-label="Invoices">
<Table.Header>
<Table.Row>
<Table.Column>
<Checkbox
aria-label="Select every invoice"
checked={selection.isAll}
mix={[on<HTMLInputElement, "change">("change", toggleAll)]}
/>
</Table.Column>
<Table.Column href={sortHref("id", sort)} sortDirection={direction("id")}>
Invoice
</Table.Column>
<Table.Column href={sortHref("customer", sort)} sortDirection={direction("customer")}>
Customer
</Table.Column>
<Table.Column href={sortHref("issued", sort)} sortDirection={direction("issued")}>
Issued
</Table.Column>
<Table.Column>Status</Table.Column>
<Table.Column align="end" href={sortHref("total", sort)} sortDirection={direction("total")}>
Total
</Table.Column>
</Table.Row>
</Table.Header>
<Table.Body>
{sortInvoices(invoices, sort).map((invoice) => (
<Table.Row
key={invoice.id}
aria-selected={selection.isSelected(invoice.id) ? "true" : undefined}
>
<Table.Cell>
<Checkbox
aria-label={`Select ${invoice.id}`}
checked={selection.isSelected(invoice.id)}
mix={[
on<HTMLInputElement, "change">("change", () => selection.toggle(invoice.id)),
]}
/>
</Table.Cell>
<Table.Cell>{invoice.id}</Table.Cell>
<Table.Cell>{invoice.customer}</Table.Cell>
<Table.Cell>{invoice.issuedLabel}</Table.Cell>
<Table.Cell>
<Badge color={statusColors[invoice.status]} variant="secondary">
{invoice.status}
</Badge>
</Table.Cell>
<Table.Cell>{invoice.totalLabel}</Table.Cell>
</Table.Row>
))}
<Table.LoadMore href="?page=2" colSpan={6}>
Load the next 25 invoices
</Table.LoadMore>
</Table.Body>
</Table>
</Table.Container>
<div mix={[hstack({ gap: 2, align: "center", justify: "end" })]}>
<Text>
{selection.size} of {invoices.length} selected
</Text>
</div>
</div>Installation
An ordinary dependency: install it, import the theme once, and import the component where it is used.
npm add @sdxc/uiimport "@sdxc/ui/theme.css";Usage
import { Table } from "@sdxc/ui";Renders the table's <table> host: a full-width, border-collapsed grid
with a small base font size, ready to hold Table.Header and
Table.Body. Dev mode warns when it lacks an accessible name.
Composition
The parts the component publishes, each a static property of the host.
TableTable.ContainerRenders an optional wrapper around Table: a relatively positioned<div>that scrolls its own inline axis when the table grows wider than the available space, and declares theui-tablenamed container.Table.HeaderRenders the table's<thead>host: a plain wrapper for the header Table.Row, separated from Table.Body by a block-end border.Table.BodyRenders the table's<tbody>host: a block-start border rules the space between direct row children, andcontent-visibility: autolets a long body's off-screen rows skip layout and paint until they scroll into view.Table.ColumnRenders a column header inside a native<th scope="col">, aligned by thedata-alignattribute.Table.RowRenders a native<tr>host: transitions its background smoothly, tints it on hover, and tints it more strongly when it carriesaria-selected="true", set directly since selection state is native.Table.CellRenders a native<td>host, padded on every side and colored as the table's emphasized foreground text.Table.LoadMoreRenders a trailing row carrying a single link that spans every column, standing in for an auto-loading "next page" trigger since fetching more rows without a full navigation requires script a consumer attaches.
Props
Read from the component's own types, so every prop, every default and every allowed value is listed.
Also accepts everything in TagProps<"table">.
Table.Container
Also accepts everything in TagProps<"div">.
Table.Header
Also accepts everything in TagProps<"thead">.
Table.Body
Also accepts everything in TagProps<"tbody">.
Table.Column
| Prop | Type | Description |
|---|---|---|
align? | "start" | "center" | "end" | Text alignment for the header and its indicator. Defaults to DEFAULT_ALIGN. |
href? | string | Target URL that re-requests the table sorted by this column. Omit for an unsortable column. |
sortDirection? | "ascending" | "descending" | This column's current sort direction, when it's the active sort key. |
parts? | ColumnPartsProps | Per-part styling for the sortable header's internal elements. |
Also accepts everything in Omit<TagProps<"th">, "align">.
Table.Row
Also accepts everything in TagProps<"tr">.
Table.Cell
Also accepts everything in TagProps<"td">.
Table.LoadMore
| Prop | Type | Description |
|---|---|---|
href | string | Target URL that renders the next page of results. |
colSpan | number | Number of columns the surrounding table renders, so the link spans the full row width. |
children | RemixNode | The link's visible label, e.g. "Load more" or "Next page". |
parts? | LoadMorePartsProps | Per-part styling for the row's internal cell and link. |
Also accepts everything in TagProps<"tr">.
Types
| Name | Values | What it decides |
|---|---|---|
Align | "start" | "center" | "end" | Text alignment for a column's header and, by convention, its cells. |
SortDirection | "ascending" | "descending" | Current sort direction for a sortable column, matching the values the native aria-sort attribute accepts. |