SemText

Reference · SemText · Data & tables

<sem-table>

SemText element

You write a normal HTML table. Sorting and filtering only change what the reader sees; the markup stays the contract, so machines always read the same rows you wrote.

A table the reader can sort and filter — the table you write is the data. You write a normal HTML table. Sorting and filtering only change what the reader sees; the markup stays the contract, so machines always read the same rows you wrote. (optional, on `<td>`): the sort key for that cell when the visible text is not the key (`15 min` vs `900`). Two numeric keys compare numerically; anything else compares as text, locale-aware. (optional): one of the comma flags `sort`, `filter`; Unknown flags are ignored. (boolean, optional): the header row sticks to the top of the scroll container. Opting in also caps the element at `max-height: 24rem` so a long table scrolls inside itself — the stick needs an internal scroll container to engage, and without it a page-level table never gains one and the header would scroll away like any other row. CSS only — works with scripts off. per the global attribute catalog (conventions.md §2) per the global attribute catalog (conventions.md §2) per the global attribute catalog (conventions.md §2) per the global attribute catalog (conventions.md §2)

Children

  • <table>Child: **exactly one `<table>`**, holding exactly one thead (its first row: header cells carrying `scope="col"`), one tbody, and optionally a caption first. A table without a thead gets no sort control. Anything else inside the table — tfoot, colgroup — is left alone.

Example

sem-table.html
source
1 <sem-table id="t-tokens" kind="comparison" controls="sort,filter" sticky>
2 <table>
3 <caption>Token lifetimes</caption>
4 <thead><tr><th scope="col">Token</th><th scope="col">Lifetime</th><th scope="col">Rotates</th></tr></thead>
5 <tbody>
6 <tr><td>access</td><td data-value="900">15 min</td><td>no</td></tr>
7 <tr><td>device</td><td data-value="604800">7 days</td><td>yes</td></tr>
8 <tr><td>refresh</td><td data-value="2592000">30 days</td><td>yes</td></tr>
9 </tbody>
10 </table>
11 </sem-table>
render

Live render boots with JavaScript — the authored source is alongside.

variant: filter without sort

`controls="filter"` drops the sort buttons: the chrome carries the labelled search box and the live status only, and the headers render as plain header cells. Typing hides every row whose text does not match (native `hidden`, so the rows leave the accessibility tree with the visual) and announces "n of m rows" about 200 ms after typing settles — not on every keystroke.

variant: filter without sort
sem-table-controls-filter.html
source
1 <sem-table id="t-sessions" controls="filter">
2 <table>
3 <caption>Active sessions</caption>
4 <thead><tr><th scope="col">Device</th><th scope="col">Last seen</th><th scope="col">Region</th></tr></thead>
5 <tbody>
6 <tr><td>MacBook Pro</td><td data-value="2026-10-14">today</td><td>EU</td></tr>
7 <tr><td>iPhone</td><td data-value="2026-10-13">yesterday</td><td>EU</td></tr>
8 <tr><td>k8s CI runner</td><td data-value="2026-10-09">5 days ago</td><td>US</td></tr>
9 </tbody>
10 </table>
11 </sem-table>
render

Live render boots with JavaScript — the authored source is alongside.

variant: no attributes at all — sort is the default

Bare `<sem-table>`: `controls` defaults to `sort`, and with no filter input and an empty status the chrome hides itself, so the reader sees only the table. Activating Count sorts numerically (3 < 9 < 14) even though the cells are plain text — a column of pure numbers is compared as numbers; a third activation restores the authored order.

variant: no attributes at all — sort is the default
sem-table-controls-default.html
source
1 <sem-table id="t-builds">
2 <table>
3 <caption>Build failures by cause</caption>
4 <thead><tr><th scope="col">Cause</th><th scope="col">Count</th></tr></thead>
5 <tbody>
6 <tr><td>flaky e2e</td><td data-value="14">14</td></tr>
7 <tr><td>type error</td><td data-value="3">3</td></tr>
8 <tr><td>timeout</td><td data-value="9">9</td></tr>
9 </tbody>
10 </table>
11 </sem-table>
render

Live render boots with JavaScript — the authored source is alongside.

variant: sticky over a long body

`sticky` caps the element at 24rem so a long table scrolls inside itself and the header sticks while it scrolls — a stick needs an internal scroll container to engage; without the cap a page-level table never gains one and the header would scroll away with the page. CSS only: the header still sticks with scripts off. Sort by When — ties keep the authored order.

variant: sticky over a long body
sem-table-sticky-long.html
source
1 <sem-table id="t-deploys" controls="sort,filter" sticky>
2 <table>
3 <caption>Recent deploys</caption>
4 <thead><tr><th scope="col">Service</th><th scope="col">When</th><th scope="col">Result</th></tr></thead>
5 <tbody>
6 <tr><td>edge-gateway</td><td data-value="2">2 h ago</td><td>ok</td></tr>
7 <tr><td>api-core</td><td data-value="5">5 h ago</td><td>ok</td></tr>
8 <tr><td>web</td><td data-value="9">9 h ago</td><td>rolled back</td></tr>
9 <tr><td>worker</td><td data-value="24">1 d ago</td><td>ok</td></tr>
10 <tr><td>docs</td><td data-value="26">1 d ago</td><td>ok</td></tr>
11 <tr><td>cdn</td><td data-value="48">2 d ago</td><td>ok</td></tr>
12 <tr><td>mail</td><td data-value="72">3 d ago</td><td>ok</td></tr>
13 <tr><td>billing</td><td data-value="76">3 d ago</td><td>ok</td></tr>
14 <tr><td>auth</td><td data-value="96">4 d ago</td><td>ok</td></tr>
15 <tr><td>search</td><td data-value="120">5 d ago</td><td>ok</td></tr>
16 </tbody>
17 </table>
18 </sem-table>
render

Live render boots with JavaScript — the authored source is alongside.

variant: kind="comparison" — first column pinned on narrow screens

`kind="comparison"` (on the element or on a plain `<table>`) pins the first column while the remaining columns scroll horizontally — shrink the window (or read at phone width) and the plan names stay in place against the columns that slide. CSS only, works with scripts off; with `sticky` it also holds while the header is stuck.

variant: kind="comparison" — first column pinned on narrow screens
sem-table-comparison-wide.html
source
1 <sem-table id="t-plans" kind="comparison">
2 <table>
3 <caption>Hosting plans</caption>
4 <thead><tr><th scope="col">Plan</th><th scope="col">Rows</th><th scope="col">Storage</th><th scope="col">Backups</th><th scope="col">Regions</th><th scope="col">Support</th></tr></thead>
5 <tbody>
6 <tr><td>Dev</td><td>10k</td><td>1 GB</td><td>weekly</td><td>1</td><td>community</td></tr>
7 <tr><td>Team</td><td>1M</td><td>50 GB</td><td>daily</td><td>3</td><td>business hours</td></tr>
8 <tr><td>Scale</td><td>unlimited</td><td>500 GB</td><td>hourly</td><td>6</td><td>24/7</td></tr>
9 </tbody>
10 </table>
11 </sem-table>
render

Live render boots with JavaScript — the authored source is alongside.

variant: data-value carries a hidden sort key

`td[data-value]` is the sort key when the visible text is not it: Shipped sorts chronologically (Jan, Feb, Mar) though the cells read "12 Jan 2026" …, and Release sorts "v2.10" after "v2.9" because text comparison is locale-aware with numeric runs. Two numeric keys compare numerically; anything else compares as text.

variant: data-value carries a hidden sort key
sem-table-text-sort-keys.html
source
1 <sem-table id="t-releases" controls="sort">
2 <table>
3 <caption>Release train</caption>
4 <thead><tr><th scope="col">Release</th><th scope="col">Shipped</th></tr></thead>
5 <tbody>
6 <tr><td>v2.9</td><td data-value="2026-01-12">12 Jan 2026</td></tr>
7 <tr><td>v2.10</td><td data-value="2026-03-02">2 Mar 2026</td></tr>
8 <tr><td>v3.0</td><td data-value="2026-02-20">20 Feb 2026</td></tr>
9 </tbody>
10 </table>
11 </sem-table>
render

Live render boots with JavaScript — the authored source is alongside.

variant: no thead — the plain-table floor

A table without a thead has no header row to sort by: no sort buttons are wired, the chrome stays empty and hides itself, and what remains is the authored table exactly as written. This is the element's floor — it degrades to plain HTML, never to a broken control.

variant: no thead — the plain-table floor
sem-table-no-thead.html
source
1 <sem-table id="t-labels" controls="sort">
2 <table>
3 <caption>Row labels</caption>
4 <tbody>
5 <tr><td>alpha</td><td>first</td></tr>
6 <tr><td>beta</td><td>second</td></tr>
7 </tbody>
8 </table>
9 </sem-table>
render

Live render boots with JavaScript — the authored source is alongside.

Source: spec/schema/sem-table.md