sdxc

Type to search, or start from one of these:

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.

InvoiceCustomerIssuedStatusTotal
INV-2041Northwind Traders12 Seppaid$1,280.00
INV-2040Acme Design11 Sepopen$480.00
INV-2039Globex9 Sepoverdue$3,900.00
INV-2038Initech4 Seppaid$96.00
INV-2037Hooli1 Seppaid$740.00
Load the next 25 invoices
1 of 5 selected
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/ui
import "@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.

  • Table
  • Table.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 the ui-table named 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, and content-visibility: auto lets 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 the data-align attribute.
  • Table.RowRenders a native <tr> host: transitions its background smoothly, tints it on hover, and tints it more strongly when it carries aria-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

PropTypeDescription
align?"start" | "center" | "end"Text alignment for the header and its indicator. Defaults to DEFAULT_ALIGN.
href?stringTarget 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?ColumnPartsPropsPer-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

PropTypeDescription
hrefstringTarget URL that renders the next page of results.
colSpannumberNumber of columns the surrounding table renders, so the link spans the full row width.
childrenRemixNodeThe link's visible label, e.g. "Load more" or "Next page".
parts?LoadMorePartsPropsPer-part styling for the row's internal cell and link.

Also accepts everything in TagProps<"tr">.

Types

NameValuesWhat 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.