Front-end runtime

One ES module, no dependencies, no build step. It is enhancement: the table is complete and correct before it runs.

What it adds

FeatureNotes
Click-to-sortType per column, decided on the server. Keyboard accessible; aria-sort maintained.
Search (Pro)Filters rows as the visitor types, debounced, with a live-region count.
Pagination (Pro)Previous / next with a page count; collapses when a filter leaves one page.
Responsive stacking (Pro)Below 40rem each row becomes a labelled card. CSS does the work; the runtime is not involved.

It is registered only on pages where a table actually needs it — a table that is not sortable, searchable or paginated loads no JavaScript at all.

Sorting, in detail

The server looks at up to fifty values in each column and decides whether the column is numbers, dates or text. If you disagree, set the column's sort as explicitly.

For numbers it reads the value out of the formatting: currency symbols, thousands separators, trailing percent signs and parenthesised negatives all resolve. Both 1,234.56 and 1.234,56 are 1234.56 — whichever separator comes last is the decimal one.

Empty cells sort last in both directions. A blank is missing data, not a small number, and burying it under a descending sort hides the rows that matter. Ties keep their original order, so re-sorting a column with repeated values does not reshuffle rows the visitor was reading.

Without JavaScript

Every row is in the HTML. The visitor loses sorting, searching and paging, and gets the whole table instead — which is also what a crawler sees, and why a Legs table indexes properly.

Restyling

The stylesheet is driven by custom properties. Set them on .legs (or on :root) and you have rethemed it without overriding a single rule:

.legs {
  --legs-accent: #30636f;
  --legs-border: #e3e5e8;
  --legs-border-strong: #c8ccd2;
  --legs-stripe: #f7f8f9;
  --legs-hover: #eef4fb;
  --legs-head-bg: #f2f4f6;
  --legs-radius: 6px;
  --legs-pad-y: 0.55rem;
  --legs-pad-x: 0.75rem;
  --legs-font-size: 1rem;
}

Dark mode is handled by prefers-color-scheme out of the box. The classes on the table — legs-striped, legs-hover, legs-bordered, legs-compact, legs-sticky — follow the table's options, so you can also style them directly.

Striping and filtered rows

“Every other row” means every other visible row once a filter is running, which no static CSS selector can know. When the runtime takes over it marks the rows itself and the stylesheet follows; with JavaScript off, plain :nth-child striping applies. Both are correct, and the switch is invisible.

Driving it yourself

import { init } from '/cpresources/…/legs.js';

// After injecting a table with your own code:
init(document.querySelector('#my-container'));

window.Legs.init() is also available for non-module code. Tables that are already running are skipped, so calling it twice is harmless.