Skip to content

HTML Table Sorting: 5 Reasons to Keep the Table

HTML Table Sorting: 5 Reasons to Keep the Table

A price list, an order history, a list of clients: someone asks for sorting and filtering, and the answer is usually a grid library. Six months later the table is a black box, the page ships 200 KB of JavaScript, and a screen reader announces a div where a table used to be.

Code: Oat-Table, MIT licensed.

HTML table sorting does not need that trade. Oat Table adds sort, filter and row selection around the <table> element, and the table stays a table.

The markup already carries the meaning

A <th scope="col"> tells a screen reader which column a cell belongs to. A <time datetime="2026-05-15"> tells a parser what the visible date means. A native checkbox in a form posts its value without a serialiser.

A grid library rebuilds all of that in JavaScript, and every rebuild loses something: the header relationship, the print stylesheet, the copy-paste into a spreadsheet.

Sorting is an attribute, not a widget

<th scope="col" data-sort> makes the column sortable as text. Add number or date and the comparison changes; auto detects the type.

When the visible text is formatted, the cell carries the value separately: $42,128 sorts by 42128. That is the whole configuration. No column definitions in a JavaScript file, no render function, no virtual DOM.

The script wraps the header text in a button and updates aria-sort, so the control is keyboard reachable and the state is announced. That is the part most custom implementations skip.

Filtering hides rows, it does not reload data

An input with data-table-filter compares the row text and hides what does not match. A row can declare its own searchable string, which is how “Ada” finds a record whose visible text starts with a company name.

The count lands in an <output aria-live="polite">, so a screen reader hears “12 rows” after each keystroke without the focus moving.

Selection uses native checkboxes, and select-all affects visible rows only. Filter for one region, select all, and the action applies to that region. With a library this behaviour is usually a bug report.

What a visitor on a bad connection gets

The table is present in the HTML, so it renders before any script parses, and it works when the script fails. On a phone with one bar of signal, that difference decides whether the list is usable.

Progressive enhancement is also a testing strategy: the page has a state that needs no JavaScript, so a broken bundle is a degraded page rather than a blank one.

What it costs

One stylesheet, one script, no runtime dependencies, and the markup you would have written anyway. For a project quoted at 30–50 $ per hour the saving is not the download, it is the week of wiring that never gets invoiced.

How to test the fallback

Disable JavaScript in the browser and load the page. The table has to render, print and be searchable with find-in-page. Then block the script request and repeat: a page that loses its content when one file fails is a page with a single point of failure in the middle of a release.

Where a library still wins

Editing cells in place, virtualised lists of ten thousand rows, pivots, column drag-and-drop, export with formatting. Those are product features, not table decoration, and a library earns its weight there.

A price list with forty rows is not that product. Keeping the native table means the next developer can read the page without opening documentation.

Related: web development for how a front-end scope is priced, and SEO for the part a table’s content plays in search. If the page also has to be readable for a translated audience, content readability covers that check.