Table

Numbers, a caption and a total row

Every number in the table uses tabular figures so the digits line up. Put data-align="end" on money columns and use tfoot for the total row.

Invoices for March 2026
Invoice Status Method Amount
INV001 Paid Credit card $25.00
INV002 Pending PayPal $15.00
INV003 Unpaid Bank transfer $35.00
INV004 Paid PayPal $145.00
Total $220.00

Sortable columns

Add aria-sort to the th and a button inside it. A click toggles ascending and descending. Numbers, dates and text each sort by their own type (a data-sort-value on a cell is used in place of its text, such as an ISO date). For server-side sorting, use an a instead of a button.

Status
Timeless Watch Mar 2, 2026 Public 1,204 $149.00
Crew tee Aug 15, 2026 Public 312 $24.00
Magic Pen Nov 30, 2025 Private 48 $9.00
Canvas bag Jan 9, 2026 Public - $38.00

A picture and a link first, an action last

Make an .avatar or img the first child of the cell, followed by a link. Align the action right with data-align="end".

Customer Email Orders
EWEmma Wilson emma@example.com 12 View
OBOlivia Brown olivia@example.com 4 View
JCJames Carter james@example.com 1 View

Truncate a long name with data-truncate

A cell with data-truncate takes the remaining space and cuts its text off with … (never narrower than 8rem). Put the full text in title.

Selectable rows

A checkbox goes in the first cell of each row, and the one in thead selects all. Selected rows get a gray fill (this works without JS). JS sets aria-selected, makes the select-all checkbox indeterminate and fires tsunami:select.

Order Customer Total
#110963 Emma Wilson $79.00
#110962 Olivia Brown $26.00
#110961 James Carter $364.50

1 selected

In a card, edge to edge

A table that is a direct child of a .card (or inside a .scroll-area) runs to the card's edges. The first and last cells line up with the card padding.

Order Customer Status Payment Total
#110963 Emma Wilson Paid Visa •••• 4417 $79.00
#110962 Olivia Brown Pending PayPal $26.00
#110961 James Carter Paid Mastercard •••• 8034 $364.50
#110960 Mark Jensen Refunded Apple Pay $13.50

Pages

Put data-page-size on the table and the script shows that many rows at a time. A nav.pagination with data-pagination-for="table-id" is filled with buttons (with no value, the one in the same card is used). data-table-info shows the range and a select with data-table-page-size changes the size. Rows are hidden, never re-created, so it stays quick up to a couple of thousand rows.

#110963 Liam Nguyen Pending Bank transfer Mar 28, 2026 $90.00
#110962 Lucas Garcia Paid Apple Pay Mar 27, 2026 $178.50
#110961 Mark Moore Paid Visa •••• 4417 Mar 26, 2026 $285.25
#110960 James Lopez Paid Mastercard •••• 8034 Mar 25, 2026 $318.75
#110959 Ethan Miller Pending PayPal Mar 24, 2026 $308.25
#110958 Zoe Miller Pending PayPal Mar 23, 2026 $58.25
#110957 Zoe Brown Paid Visa •••• 2290 Mar 22, 2026 $325.25
#110956 Mark Brown Paid Visa •••• 4417 Mar 21, 2026 $152.50
#110955 Mark Clark Refunded Bank transfer Mar 20, 2026 $306.00
#110954 James Jensen Refunded Visa •••• 4417 Mar 19, 2026 $29.75
#110953 Grace Lopez Paid Bank transfer Mar 18, 2026 $319.25
#110952 Zoe Moore Paid PayPal Mar 17, 2026 $316.00
#110951 Zoe Wilson Pending Bank transfer Mar 16, 2026 $258.75
#110950 Zoe Garcia Pending Apple Pay Mar 15, 2026 $27.25
#110949 Henry Moore Paid Bank transfer Mar 14, 2026 $173.75
#110948 Noah Jensen Refunded PayPal Mar 13, 2026 $156.25
#110947 Henry Moore Paid Bank transfer Mar 12, 2026 $277.00
#110946 Lucas Wilson Pending Mastercard •••• 8034 Mar 11, 2026 $68.25
#110945 James Clark Pending PayPal Mar 10, 2026 $238.00
#110944 Lucas Miller Paid Visa •••• 2290 Mar 9, 2026 $246.25
#110943 Emma Khan Paid Bank transfer Mar 8, 2026 $60.25
#110942 Chloe Jensen Paid Visa •••• 2290 Mar 7, 2026 $88.25
#110941 Ethan Miller Pending Visa •••• 2290 Mar 6, 2026 $306.00
#110940 Liam Miller Pending Mastercard •••• 8034 Mar 5, 2026 $252.00
#110939 Sofia Brown Paid Mastercard •••• 8034 Mar 4, 2026 $21.25

Search

An input[data-table-search="table-id"] keeps the rows that contain the text in any cell, ignoring case. It waits 150 ms after typing. When nothing matches, the content of template[data-table-empty] is shown in one cell that spans the visible columns.

#110963 Liam Nguyen Pending Bank transfer Mar 28, 2026 $90.00
#110962 Lucas Garcia Paid Apple Pay Mar 27, 2026 $178.50
#110961 Mark Moore Paid Visa •••• 4417 Mar 26, 2026 $285.25
#110960 James Lopez Paid Mastercard •••• 8034 Mar 25, 2026 $318.75
#110959 Ethan Miller Pending PayPal Mar 24, 2026 $308.25
#110958 Zoe Miller Pending PayPal Mar 23, 2026 $58.25
#110957 Zoe Brown Paid Visa •••• 2290 Mar 22, 2026 $325.25
#110956 Mark Brown Paid Visa •••• 4417 Mar 21, 2026 $152.50
#110955 Mark Clark Refunded Bank transfer Mar 20, 2026 $306.00
#110954 James Jensen Refunded Visa •••• 4417 Mar 19, 2026 $29.75
#110953 Grace Lopez Paid Bank transfer Mar 18, 2026 $319.25
#110952 Zoe Moore Paid PayPal Mar 17, 2026 $316.00

Column filters

A second header row, tr[data-filters], can hold an input or a select under any column; the cell must sit at that column's index. An input keeps rows whose cell contains the text and a select keeps rows whose cell equals the chosen option (the empty option clears it). Filters combine with the search and send the table back to page 1.

#110963 Liam Nguyen Pending Bank transfer Mar 28, 2026 $90.00
#110962 Lucas Garcia Paid Apple Pay Mar 27, 2026 $178.50
#110961 Mark Moore Paid Visa •••• 4417 Mar 26, 2026 $285.25
#110960 James Lopez Paid Mastercard •••• 8034 Mar 25, 2026 $318.75
#110959 Ethan Miller Pending PayPal Mar 24, 2026 $308.25
#110958 Zoe Miller Pending PayPal Mar 23, 2026 $58.25
#110957 Zoe Brown Paid Visa •••• 2290 Mar 22, 2026 $325.25
#110956 Mark Brown Paid Visa •••• 4417 Mar 21, 2026 $152.50
#110955 Mark Clark Refunded Bank transfer Mar 20, 2026 $306.00
#110954 James Jensen Refunded Visa •••• 4417 Mar 19, 2026 $29.75
#110953 Grace Lopez Paid Bank transfer Mar 18, 2026 $319.25
#110952 Zoe Moore Paid PayPal Mar 17, 2026 $316.00

Column visibility

A .dropdown with data-table-columns="table-id" lists the columns. If it has no checkboxes the script writes one per header (value = column index, label = header text); you can also write them yourself. An unchecked column gets hidden on its th and td, and the last visible column cannot be hidden. Add data-table-remember to a table with an id to keep the choice in localStorage.

Orders

#110963 Liam Nguyen Pending Bank transfer Mar 28, 2026 $90.00
#110962 Lucas Garcia Paid Apple Pay Mar 27, 2026 $178.50
#110961 Mark Moore Paid Visa •••• 4417 Mar 26, 2026 $285.25
#110960 James Lopez Paid Mastercard •••• 8034 Mar 25, 2026 $318.75
#110959 Ethan Miller Pending PayPal Mar 24, 2026 $308.25
#110958 Zoe Miller Pending PayPal Mar 23, 2026 $58.25
#110957 Zoe Brown Paid Visa •••• 2290 Mar 22, 2026 $325.25
#110956 Mark Brown Paid Visa •••• 4417 Mar 21, 2026 $152.50
#110955 Mark Clark Refunded Bank transfer Mar 20, 2026 $306.00
#110954 James Jensen Refunded Visa •••• 4417 Mar 19, 2026 $29.75

Sticky header

data-sticky keeps the header (and the filter row) at the top of its scroll container: a .scroll-area with a max-height, or the page. data-sticky="both" also keeps the first column at the left edge when the table scrolls sideways.

#110963 Liam Nguyen Pending Bank transfer Mar 28, 2026 $90.00
#110962 Lucas Garcia Paid Apple Pay Mar 27, 2026 $178.50
#110961 Mark Moore Paid Visa •••• 4417 Mar 26, 2026 $285.25
#110960 James Lopez Paid Mastercard •••• 8034 Mar 25, 2026 $318.75
#110959 Ethan Miller Pending PayPal Mar 24, 2026 $308.25
#110958 Zoe Miller Pending PayPal Mar 23, 2026 $58.25
#110957 Zoe Brown Paid Visa •••• 2290 Mar 22, 2026 $325.25
#110956 Mark Brown Paid Visa •••• 4417 Mar 21, 2026 $152.50
#110955 Mark Clark Refunded Bank transfer Mar 20, 2026 $306.00
#110954 James Jensen Refunded Visa •••• 4417 Mar 19, 2026 $29.75
#110953 Grace Lopez Paid Bank transfer Mar 18, 2026 $319.25
#110952 Zoe Moore Paid PayPal Mar 17, 2026 $316.00
#110951 Zoe Wilson Pending Bank transfer Mar 16, 2026 $258.75
#110950 Zoe Garcia Pending Apple Pay Mar 15, 2026 $27.25
#110949 Henry Moore Paid Bank transfer Mar 14, 2026 $173.75
#110948 Noah Jensen Refunded PayPal Mar 13, 2026 $156.25
#110947 Henry Moore Paid Bank transfer Mar 12, 2026 $277.00
#110946 Lucas Wilson Pending Mastercard •••• 8034 Mar 11, 2026 $68.25
#110945 James Clark Pending PayPal Mar 10, 2026 $238.00
#110944 Lucas Miller Paid Visa •••• 2290 Mar 9, 2026 $246.25
#110943 Emma Khan Paid Bank transfer Mar 8, 2026 $60.25
#110942 Chloe Jensen Paid Visa •••• 2290 Mar 7, 2026 $88.25
#110941 Ethan Miller Pending Visa •••• 2290 Mar 6, 2026 $306.00
#110940 Liam Miller Pending Mastercard •••• 8034 Mar 5, 2026 $252.00
#110939 Sofia Brown Paid Mastercard •••• 8034 Mar 4, 2026 $21.25
#110938 Ethan Miller Paid Mastercard •••• 8034 Mar 3, 2026 $343.75
#110937 Sofia Clark Refunded Apple Pay Feb 28, 2026 $89.75
#110936 Grace Jensen Paid PayPal Feb 27, 2026 $12.50
#110935 Daniel Jensen Paid Apple Pay Feb 26, 2026 $161.75
#110934 Mark Patel Refunded Visa •••• 2290 Feb 25, 2026 $17.00

Everything together

Sorting works on the whole filtered set before it is cut into pages. The select-all box selects every row that matches the filter, on all pages, not only the visible one. After any change the table fires tsunami:tablechange.

Orders

#110963 Liam Nguyen Pending Bank transfer Mar 28, 2026 $90.00
#110962 Lucas Garcia Paid Apple Pay Mar 27, 2026 $178.50
#110961 Mark Moore Paid Visa •••• 4417 Mar 26, 2026 $285.25
#110960 James Lopez Paid Mastercard •••• 8034 Mar 25, 2026 $318.75
#110959 Ethan Miller Pending PayPal Mar 24, 2026 $308.25
#110958 Zoe Miller Pending PayPal Mar 23, 2026 $58.25
#110957 Zoe Brown Paid Visa •••• 2290 Mar 22, 2026 $325.25
#110956 Mark Brown Paid Visa •••• 4417 Mar 21, 2026 $152.50
#110955 Mark Clark Refunded Bank transfer Mar 20, 2026 $306.00
#110954 James Jensen Refunded Visa •••• 4417 Mar 19, 2026 $29.75
#110953 Grace Lopez Paid Bank transfer Mar 18, 2026 $319.25
#110952 Zoe Moore Paid PayPal Mar 17, 2026 $316.00
#110951 Zoe Wilson Pending Bank transfer Mar 16, 2026 $258.75
#110950 Zoe Garcia Pending Apple Pay Mar 15, 2026 $27.25
#110949 Henry Moore Paid Bank transfer Mar 14, 2026 $173.75
#110948 Noah Jensen Refunded PayPal Mar 13, 2026 $156.25
#110947 Henry Moore Paid Bank transfer Mar 12, 2026 $277.00
#110946 Lucas Wilson Pending Mastercard •••• 8034 Mar 11, 2026 $68.25
#110945 James Clark Pending PayPal Mar 10, 2026 $238.00
#110944 Lucas Miller Paid Visa •••• 2290 Mar 9, 2026 $246.25
#110943 Emma Khan Paid Bank transfer Mar 8, 2026 $60.25
#110942 Chloe Jensen Paid Visa •••• 2290 Mar 7, 2026 $88.25
#110941 Ethan Miller Pending Visa •••• 2290 Mar 6, 2026 $306.00
#110940 Liam Miller Pending Mastercard •••• 8034 Mar 5, 2026 $252.00
#110939 Sofia Brown Paid Mastercard •••• 8034 Mar 4, 2026 $21.25
#110938 Ethan Miller Paid Mastercard •••• 8034 Mar 3, 2026 $343.75
#110937 Sofia Clark Refunded Apple Pay Feb 28, 2026 $89.75
#110936 Grace Jensen Paid PayPal Feb 27, 2026 $12.50
#110935 Daniel Jensen Paid Apple Pay Feb 26, 2026 $161.75
#110934 Mark Patel Refunded Visa •••• 2290 Feb 25, 2026 $17.00
#110933 Olivia Clark Refunded Apple Pay Feb 24, 2026 $306.25
#110932 Emma Lopez Paid Bank transfer Feb 23, 2026 $216.75
#110931 Ava Taylor Refunded Visa •••• 2290 Feb 22, 2026 $112.25
#110930 Lucas Garcia Pending PayPal Feb 21, 2026 $102.25
#110929 Olivia Garcia Paid PayPal Feb 20, 2026 $15.75
#110928 Henry Carter Paid Bank transfer Feb 19, 2026 $111.75
#110927 Liam Carter Pending Bank transfer Feb 18, 2026 $387.50
#110926 Ava Miller Pending Apple Pay Feb 17, 2026 $70.00
#110925 Liam Garcia Paid Visa •••• 2290 Feb 16, 2026 $287.75
#110924 Ava Moore Paid Mastercard •••• 8034 Feb 15, 2026 $96.50

0 selected

Attributes

Attribute / element Values Description
.table table Full width, tabular numbers, small gray headers and row dividers. Rows get a gray fill on hover
caption Table description. Small, gray, left-aligned above the headers
thead th Column header. 12px, gray, not bold, no wrapping
tbody th[scope="row"] Cell that names the row. Dark, weight 500
tfoot Total row: top border, bold
data-align end; omit for left-aligned (default) On th/td: right-aligned, for money and numbers
data-truncate (boolean) On td: takes the remaining space, cut off with …, never narrower than 8rem; put the full text in title
td > .avatar / img:first-child Picture before the text in a cell (img is 32px with rounded corners)
th[aria-sort] none, ascending, descending Sortable column with a direction icon. JS sets it on click
th[aria-sort] > button Sort button. A click toggles ascending/descending (use a for server-side sorting)
th[data-sort-type] number, date, text; omit to detect (default) Forces the column's sort type
td[data-sort-value] Any value Value to sort by instead of the text, such as 2026-03-02
td:first-child > input[type=checkbox] Row checkbox. Selected rows get a gray fill (CSS :has)
th:first-child > input[type=checkbox] In thead: selects all. Indeterminate when some rows are selected
tr[aria-selected] true, false Set by JS from the row's checkbox
data-page-size A number On the table: rows per page. Rows outside the page get hidden
nav.pagination[data-pagination-for] Table id; omit the value to use the table in the same card Filled with prev, page numbers (with …), next as buttons. Hidden when there is one page
[data-table-info] Table id Shows the range, such as 1–10 of 42
select[data-table-page-size] Table id Changes data-page-size and goes back to page 1
input[data-table-search] Table id Global search: rows whose cells contain the text, ignoring case (150 ms debounce)
thead tr[data-filters] Row of column filters. An input in a cell filters by contains, a select by equals, on the column at the cell's index
template[data-table-empty] Optional table id Content for the row shown when no row matches, spanning the visible columns. A tr[data-table-empty] in tbody works too
[data-table-columns] Table id A .dropdown of column checkboxes (value = column index). Written from the headers when empty. Columns are hidden with hidden
data-table-remember (boolean) On a table with an id: keep the hidden columns in localStorage
data-sticky both; omit for the header only On the table: sticky header inside its scroll container. both adds the first column
data-enhanced Set by JS on a table that pages or filters; rows then draw their divider on top so a hidden last row leaves no stray line
.card > .table, .card > .scroll-area > .table Table runs to the card's edges
.scroll-area > .table Wraps a wide table so it scrolls horizontally on narrow screens

API

src/js/table.js uses event delegation on document, so it works on tables added later right away. The functions live at window.tsunami.table. Sorting, filtering and paging work together: the rows are sorted, filtered, then cut into pages.

Function / event Arguments / detail Description
sort(table, columnIndex, direction) table: element, columnIndex: index of the header cell, direction: "ascending" (default) or "descending" Sorts the rows in tbody and sets aria-sort. Returns false if the event was canceled
select(table, rows, checked) rows: a list of tr, checked: boolean (default true) Selects or deselects rows, then fires tsunami:select
selected(table) table: element Returns an array of the selected tr elements
sync(table) table: element Resets aria-selected and the select-all checkbox state after you change checkboxes yourself (runs on page load automatically)
refresh(table) table: element Reads the rows again after you add, remove or change them, and redraws the page
page(table, n) n: page number (kept within range) Shows page n
filter(table, text) text: the search text; "" clears it Sets the global search (and the search input), goes to page 1
tsunami:tablechange { page, pageSize, total, filtered, sort, hidden } Fired on the table after a page, filter, sort or column change. Bubbles. total counts all rows, filtered those matching; sort is { column, direction } or null; hidden lists hidden column indexes
tsunami:sort { column, direction, header } Fired on the table before sorting. Bubbles and is cancelable: call preventDefault() to sort yourself (such as fetching from the server)
tsunami:select { rows, count } Fired on the table whenever the selection changes. Bubbles