Skip to content

Table

A data table with client sorting, row selection, a sticky header, built-in empty and loading states, and an opt-in stacked mode for narrow widths. Real <table> semantics throughout.

Import

import { Table } from "@hyzer-labs/ui"

Demo

Large datasets

Real <table> semantics give you native screen-reader table navigation for free, so reach for this component up to some thousands of rows. Past the point where rendering every <tr> becomes the bottleneck, see the Virtualized table pattern, which windows an ARIA table with Virtualizer instead.

A framed table with rules between the rows. Inside a card or panel that already draws its own edges, bordered=false drops the frame and those rules. The divider under the header stays either way.

Discs
VoyagerDistance919.99
AviarPutter29.99
RocMidrange514.99
WraithDistance1121.99
BuzzzMidrange515.99
JudgePutter211.99
FirebirdDistance918.99
ZoneMidrange413.99
interface Disc {
	id: string;
	name: string;
	type: string;
	speed: number;
	price: number;
}

const items: Disc[] = [
	{ id: 'd1', name: 'Voyager', type: 'Distance', speed: 9, price: 19.99 },
	{ id: 'd2', name: 'Aviar', type: 'Putter', speed: 2, price: 9.99 },
	// …
];

// Each column's `key` names a Disc property: Table renders `row[key]`
// by default (String(row.name), String(row.price), …).
const columns: TableColumn<Disc>[] = [
	{ key: 'name', header: 'Name', sortable: true },
	{ key: 'type', header: 'Type', sortable: true },
	{ key: 'speed', header: 'Speed', sortable: true, align: 'end' },
	{ key: 'price', header: 'Price', sortable: true, align: 'end', width: '7rem' }
];

<Table {items} {columns} caption="Discs" />

Props

NameTypeDefaultNote
itemsT[]Required.
columnsTableColumn<T>[]Required. Each column's `key` is a property name on T — the default cell reads `row[key]`. See TableColumn below.
captionstring | SnippetVisible caption. `caption` or `ariaLabel` is required — logs a warning in development if neither is set. Caption wins if both are given (no aria-label).
ariaLabelstringSee `caption`.
sortTableSort | nullnullBindable. See TableSort below.
clientSortbooleantruefalse: Table renders `items` as given and only reports/marks sort state — you reorder `items` yourself.
selectablebooleanfalsePrepends a checkbox column.
selectedSvelteSet<string>new SvelteSet()Bindable — row ids (via getRowId) currently selected.
getRowId(row: T, index: number) => stringstringified indexRow identity for selection. Supply a real id so selection survives re-sorting.
rowLabel(row: T) => stringAccessible name for a row's checkbox; falls back to the first column's cell text.
stickyHeaderbooleanfalse
borderedbooleantrueFrame and row rules. Set false inside a card or panel that already draws the edges; the divider under the header stays either way.
loadingbooleanfalse
loadingRowsnumber3
stack'sm' | 'md' | 'lg'Stacks below the named width. Off by default (scroll wrap only); 'sm' (640px) suits most tables — reserve md/lg for wide or many-column tables that need to shed the table layout earlier.
cellSnippet<[T, TableColumn<T>]>Default is `String(row[column.key])`.
emptySnippetDefault is "No rows".
classstringMerged after hz-table-wrap.

TableColumn<T>

NameTypeDefaultNote
keystringRequired. A property name on a row of T (the default cell renders `row[key]`) and the column id (sort target, stacked-mode data-label).
headerstringRequired. Header cell text (also the stacked-mode label).
sortablebooleanfalse
sortBy(row: T) => string | numberrow[key]The escape hatch for mixed-type or computed sort values.
align'start' | 'center' | 'end''start'Logical — flips under RTL.
widthstringCSS width for the column's <col>.

TableSort

NameTypeDefault
keystring
direction'asc' | 'desc'

Theme hooks

What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.

Root class: .hz-table hz-table-wrap

Data attributes

HookValuesStyles
data-borderedpresent unless bordered={false}On .hz-table-wrap. The reference theme keys the frame and the row rules off it, so `bordered={false}` leaves an edgeless table for a card or panel that draws its own edges. The heavier divider under the header stays either way, since a borderless table still needs its header separated from its body.
data-stickypresent when stickyHeaderOn .hz-table-wrap. Pins thead against the wrap’s own scroll — cap the wrap’s max-height (e.g. via your own class through Table’s `class` prop, which lands on the wrap) to see it scroll.
data-stack'sm' | 'md' | 'lg'On .hz-table-wrap. Mirrors the stack prop — stacked below the named --hz-width-* threshold, a real table at/above it. Absent (default): never stacks. 'sm' (640px) is the recommended default — genuinely narrow viewports only.
data-selectedpresent on a selected rowOn tbody tr. Pairs with aria-selected="true" — both present only when selected.
data-align'start' | 'center' | 'end'On th/td, mirroring a column’s align. Logical values, so start/end flip under RTL with no extra rule.
aria-sort'ascending' | 'descending'Not a data-* hook, but load-bearing for styling the active-column indicator: present only on the sorted column’s th, never on the others.

Part classes

HookValuesStyles
.hz-table-sortchild elementThe sort trigger — a real button wrapping a sortable column’s header text, with the active-column chevron alongside it.
.hz-table-emptychild elementThe full-width cell rendered when items is empty and not loading.
.hz-table-skeletonchild elementA loading placeholder row — on tbody tr; aria-hidden, so it never reaches assistive tech.
.hz-table--stripedopt-in classZebra striping. Pass it via class — like Card’s treatment classes, this is a theme look, not a prop.

Accessibility

This is a static data table, not an APG grid pattern. Sort buttons and checkboxes are ordinary native controls in the natural tab order, so there is no roving grid navigation.

aria-sort (ascending/descending) sits only on the sorted column's th, never on the others. Select-all announces the partial state through the native indeterminate property, not an ARIA attribute.

Loading skeleton rows carry aria-hidden, so they never reach assistive tech.

Real role/scope semantics are stamped explicitly (table/row/columnheader/cell, plus scope), so they survive the stacked mode's CSS display overrides.

References: APG sortable table example · MDN — accessible data tables