DEV3 Technologies home

Sortable table with sticky header

A data table with click-to-sort column headers, a sticky header inside a scroll box and correct aria-sort state.

Tailwind CSSTablestablesortingstickya11y

Live preview

Code

<div class="max-h-72 overflow-auto rounded-xl border border-slate-200">
  <table id="orders" class="w-full text-left text-sm">
    <thead>
      <tr>
        <th scope="col" aria-sort="none" class="sticky top-0 bg-slate-50 px-4 py-3">
          <button type="button" class="font-semibold text-slate-700">Customer</button></th>
        <th scope="col" aria-sort="none" class="sticky top-0 bg-slate-50 px-4 py-3">
          <button type="button" class="font-semibold text-slate-700">Total</button></th>
        <th scope="col" aria-sort="none" class="sticky top-0 bg-slate-50 px-4 py-3">
          <button type="button" class="font-semibold text-slate-700">Date</button></th>
      </tr>
    </thead>
    <tbody class="divide-y divide-slate-100 bg-white">
      <tr><td class="px-4 py-3">Northwind</td><td class="px-4 py-3" data-sort="1250">$1,250</td><td class="px-4 py-3" data-sort="2026-03-02">2 Mar</td></tr>
      <tr><td class="px-4 py-3">Acme</td><td class="px-4 py-3" data-sort="90">$90</td><td class="px-4 py-3" data-sort="2026-01-15">15 Jan</td></tr>
      <tr><td class="px-4 py-3">Globex</td><td class="px-4 py-3" data-sort="4300">$4,300</td><td class="px-4 py-3" data-sort="2026-02-20">20 Feb</td></tr>
      <tr><td class="px-4 py-3">Initech</td><td class="px-4 py-3" data-sort="610">$610</td><td class="px-4 py-3" data-sort="2026-03-28">28 Mar</td></tr>
    </tbody>
  </table>
</div>

How it works

What it solves

Admin tables grow beyond one screen and users want to order rows by a column. Many sorters forget the header disappears while scrolling or leave screen readers guessing the order. This pattern keeps the header pinned and reports the sort state properly.

How it works

The table lives in a max-h-72 overflow-auto wrapper, and the thead cells use sticky top-0 with a solid background so rows pass beneath them. Each sortable header contains a real button, and the parent th carries aria-sort, which the script updates between ascending, descending and none. The script reads each cell's data-sort value when present (so currency or dates sort by raw value) and falls back to text, compares numbers numerically via Intl.Collator with the numeric option, then re-appends the rows in order, which moves existing nodes instead of rebuilding them. Clicking the same header again flips direction.

Adapting it

Mark numeric cells with data-sort holding a plain number. For large data sets, sort on the server and only use the aria-sort update from this script. Add a caret icon through a th[aria-sort=ascending] selector.

Pitfalls

  • Sticky only works if the scrolling ancestor is the wrapper, not the page.
  • Cells need background colour or text shows through the pinned header.
  • Sorting text with < misorders accents; use the collator.

How to use it

  1. Paste table.html into a Tailwind 4 page and keep the scroll wrapper.
  2. Add data-sort to cells whose display text is not the sort key (money, dates).
  3. Load sort-table.js with defer.
  4. Give extra columns a button inside th to make them sortable.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets