Skip to main content

Table

Info:Content
27

A styled data table around native `<table>`: zebra, bordered, hover, sticky header, and a sortable-header affordance.

data table rows columns spreadsheet sortable grid

Live demo

live · @xtyle/astro

Table

Sortable, hover, sticky header

Click a sortable column header (or focus it and press Enter) to re-order the rows.

Algorithm Tokens Coverage Status
xtyle-default 142 Success:100% Success:Blessed
xtyle-quiet 138 Warning:98% Success:Blessed
xtyle-loud 151 Warning:96% Warning:Draft
xtyle-hc 147 Success:100% Success:Blessed
aurora 129 Warning:91% Community

Striped, compact

Component Category Consumed tokens
buttoncontrol9
fieldform11
alertfeedback7
dialogoverlay8
tablecontent12

Bordered

Tier Audience Surface
Buildersalgorithm authorsinternal derivation
Knobseveryonedeclared inputs
Overridespower usersany token, anywhere

Table dresses a native <table> in xtyle styling without taking over its markup. The binding is a thin wrapper that relays consumer-authored rows: you write the real <thead>/<tbody>/<tr>/<th>/<td> and tag them with the xtyle-table__* part classes, and the wrapper supplies the scroll container, variant, size, hover, and sticky behavior.

Rows you render after mount get decorated automatically by a subtree observer, so a keyed {#each} or a live feed needs no classing at all. A framework that would rather class rows at author time (skipping the observer entirely) can, and to keep that path from hard-coding magic strings, @xtyle/core exports the part-class names as the typed tableParts map (tableParts.head, .row, .cell, .headerCell, .body, …); the same constants the decorator itself writes, so a part rename can't drift silently. Variants default/striped/bordered set the row and cell chrome; compact/normal sizes set the cell padding. The sortable-header affordance renders a sort glyph and exposes an aria-sort hook on the header cell; the engine is presentation-only, so the consumer wires the actual sort and toggles aria-sort (ascending/descending/none), and the glyph rotates to match. scope belongs on every header cell so screen readers associate it with its row or column.

When to use

How this component composes with the rest of the set.

Pair header cells with the sortable affordance and wire sorting yourself: toggle aria-sort between ascending/descending/none; the glyph follows.
Drop a Badge or Avatar into any cell for status pills and row identities.
Wrap the table in a Card for an elevated, contained data surface.

Props

5 props, straight from the manifest.

PropTypeDefaultBindingsDescription
variant TableVariant
default striped bordered
default
html svelte astro
Row and cell chrome. `striped` zebra-stripes body rows; `bordered` outlines every cell.
size TableSize
normal compact
normal
html svelte astro
Cell density. `compact` tightens the cell padding.
hover boolean false
html svelte astro
Highlights the body row under the pointer with the hover state tint.
sticky boolean false
html svelte astro
Pins the header cells to the top of the scroll container while the body scrolls.
ariaLabel string
html svelte astro
Accessible name for the table. Required when there is no `<caption>`; the binding warns when both are missing.

Appearance

Variants

default

.xtyle-table

Plain rows separated by a bottom rule; no class needed.

striped

.xtyle-table--striped

Alternating body-row backgrounds for scannability.

bordered

.xtyle-table--bordered

A full grid; every cell gets a border.

Sizes

normal

default
.xtyle-table

Default density.

compact

.xtyle-table--compact

Tighter cell padding.

States

hover

.xtyle-table--hover .xtyle-table__body .xtyle-table__row:hover

Pointer over a body row (with hover); the row paints the hover tint.

sortable

.xtyle-table__header-cell--sortable

A sortable header cell: pointer cursor and a hover tint behind the label.

sorted

.xtyle-table__header-cell[aria-sort="ascending"]

A header cell with aria-sort set; the glyph colors and points up (asc) or down (desc).

focus-visible

.xtyle-table__header-cell--sortable:focus-visible

Keyboard focus on a sortable header: an inset token ring, plus a transparent outline that becomes real in forced-colors mode.

scroll-focus

.xtyle-table-wrap:focus-visible

Keyboard focus on the scrollable wrap, which turns tabbable only when the table overflows: an outline sitting outside the border box, so the scrolled table can't cover it.

Anatomy

The named parts that make up the component, with their selectors.

wrap

.xtyle-table-wrap

The horizontal-scroll container that clips the table to its width and rounds the corners.

--radius-lg

table

.xtyle-table

The <table> itself: collapsed borders, surface background, base typography.

--font-sans --text-body --leading-normal --fg-0 --bg-1

caption

.xtyle-table__caption

An optional <caption> rendered above the table as a muted label.

--space-2 --space-4 --text-sm --fg-2

head

.xtyle-table__head

The <thead> group; its last header row carries the heavier under-rule.

--border-normal --line-2

body

.xtyle-table__body

The <tbody> group that zebra striping and hover target.

row

.xtyle-table__row

A <tr>: the unit zebra striping and row hover apply to.

header-cell

.xtyle-table__header-cell

A <th>: semibold ink on a tint, with scope and an optional sortable affordance.

--space-3 --space-4 --weight-semibold --fg-1 --bg-2 --line

cell

.xtyle-table__cell

A <td>: the standard data cell with a row-separating bottom rule.

--space-3 --space-4 --line

sort

.xtyle-table__sort

The decorative sort glyph inside a sortable header; rotates and colors per aria-sort.

--fg-3 --accent-text --duration-fast --ease-standard

footer-cell

.xtyle-table__footer-cell

A <td>/<th> in <tfoot>: a summary cell on a tint above a heavy top rule.

--space-3 --space-4 --weight-medium --fg-1 --bg-2 --border-normal --line-2

Tokens & coverage

What the component consumes, checked live against what the algorithm produces.

Success:fully covered 27/27 consumed tokens produced default register: 299 tokens

Live coverage check against the xtyle-default register (derive(xtyleDefault, { anchors })coverComponent(manifest, register)). Every token this component consumes must be a key the algorithm produces.

--accent-text --bg-1 --bg-2 --border-normal --border-thick --border-thin --duration-fast --ease-standard --fg-0 --fg-1 --fg-2 --fg-3 --font-sans --leading-normal --line --line-2 --radius-lg --ring --space-1 --space-2 --space-3 --space-4 --state-hover --text-body --text-sm --weight-medium --weight-semibold

Slots

default
html svelte astro

The consumer-authored <table> (with xtyle-table__* part classes on its rows and cells).

Accessibility

Built on a native <table> with <thead>/<tbody>/<tfoot>, <th scope>, and an optional <caption>. Row/column semantics come for free.
Every header cell SHOULD carry scope (col or row) so assistive tech associates data cells correctly.
A table with no <caption> needs an aria-label; the binding warns at runtime when both are missing.
Sortable headers expose state via aria-sort (ascending/descending/none), not via the glyph, which is aria-hidden.
The sortable header is keyboard-focusable; focus shows a token ring plus a transparent outline the forced-colors base rule promotes to a real system outline.
A table wide or tall enough to scroll becomes a keyboard-scrollable region (a tabindex stop only while it overflows), and focus on it shows an outline, so a keyboard user can see they've landed there before arrowing through it (WCAG 2.4.7).

Code

Variants, sticky header, and a sortable column

Striped rows with hover and a sticky, sortable header; the consumer supplies rows and owns the sort.

<xtyle-table variant="striped" hover sticky aria-label="Recent orders">
	<table>
		<caption class="xtyle-table__caption">Recent orders</caption>
		<thead class="xtyle-table__head">
			<tr class="xtyle-table__row">
				<th class="xtyle-table__header-cell xtyle-table__header-cell--sortable" scope="col" aria-sort="ascending">
					<span class="xtyle-table__header-content">Order
						<span class="xtyle-table__sort" aria-hidden="true">
							<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 8l5 6H7l5-6Z" /></svg>
						</span>
					</span>
				</th>
				<th class="xtyle-table__header-cell" scope="col">Customer</th>
				<th class="xtyle-table__header-cell" scope="col">Total</th>
			</tr>
		</thead>
		<tbody class="xtyle-table__body">
			<tr class="xtyle-table__row">
				<td class="xtyle-table__cell">#1024</td>
				<td class="xtyle-table__cell">Ada Lovelace</td>
				<td class="xtyle-table__cell">$42.00</td>
			</tr>
			<tr class="xtyle-table__row">
				<td class="xtyle-table__cell">#1025</td>
				<td class="xtyle-table__cell">Alan Turing</td>
				<td class="xtyle-table__cell">$17.50</td>
			</tr>
		</tbody>
	</table>
</xtyle-table>
<script lang="ts">
	import { Table } from "@xtyle/svelte";

	let sort: "ascending" | "descending" = $state("ascending");
</script>

<Table variant="striped" hover sticky ariaLabel="Recent orders">
	<table>
		<caption class="xtyle-table__caption">Recent orders</caption>
		<thead class="xtyle-table__head">
			<tr class="xtyle-table__row">
				<th
					class="xtyle-table__header-cell xtyle-table__header-cell--sortable"
					scope="col"
					aria-sort={sort}
					tabindex="0"
					onclick={() => (sort = sort === "ascending" ? "descending" : "ascending")}
				>
					<span class="xtyle-table__header-content">Order
						<span class="xtyle-table__sort" aria-hidden="true">
							<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 8l5 6H7l5-6Z" /></svg>
						</span>
					</span>
				</th>
				<th class="xtyle-table__header-cell" scope="col">Customer</th>
				<th class="xtyle-table__header-cell" scope="col">Total</th>
			</tr>
		</thead>
		<tbody class="xtyle-table__body">
			<tr class="xtyle-table__row">
				<td class="xtyle-table__cell">#1024</td>
				<td class="xtyle-table__cell">Ada Lovelace</td>
				<td class="xtyle-table__cell">$42.00</td>
			</tr>
		</tbody>
	</table>
</Table>
---
import { Table } from "@xtyle/astro";
---

<Table variant="bordered" size="compact" aria-label="Pricing">
	<table>
		<thead class="xtyle-table__head">
			<tr class="xtyle-table__row">
				<th class="xtyle-table__header-cell" scope="col">Plan</th>
				<th class="xtyle-table__header-cell" scope="col">Price</th>
			</tr>
		</thead>
		<tbody class="xtyle-table__body">
			<tr class="xtyle-table__row">
				<th class="xtyle-table__header-cell" scope="row">Starter</th>
				<td class="xtyle-table__cell">$0</td>
			</tr>
			<tr class="xtyle-table__row">
				<th class="xtyle-table__header-cell" scope="row">Pro</th>
				<td class="xtyle-table__cell">$12</td>
			</tr>
		</tbody>
	</table>
</Table>

Author-time classing with the typed `tableParts`

When you render rows yourself, class them from the exported tableParts map instead of string literals; a part rename is then a compile error, not a silent style regression.

<script lang="ts">
	import { Table } from "@xtyle/svelte";
	import { tableParts } from "@xtyle/core";

	let rows = $state([
		{ id: "#1024", customer: "Ada Lovelace", total: "$42.00" },
		{ id: "#1025", customer: "Alan Turing", total: "$17.50" },
	]);
</script>

<Table variant="striped" hover ariaLabel="Recent orders">
	<table>
		<thead class={tableParts.head}>
			<tr class={tableParts.row}>
				<th class={tableParts.headerCell} scope="col">Order</th>
				<th class={tableParts.headerCell} scope="col">Customer</th>
				<th class={tableParts.headerCell} scope="col">Total</th>
			</tr>
		</thead>
		<tbody class={tableParts.body}>
			{#each rows as row (row.id)}
				<tr class={tableParts.row}>
					<td class={tableParts.cell}>{row.id}</td>
					<td class={tableParts.cell}>{row.customer}</td>
					<td class={tableParts.cell}>{row.total}</td>
				</tr>
			{/each}
		</tbody>
	</table>
</Table>