webdevpuneet.com

Multi-Column Sort Table — Shift-Click Priority Sort HTML CSS JS

Multi-Column Sort Table · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="tms-card">
  <div class="tms-head">
    <h3>Player leaderboard</h3>
    <p class="tms-hint">Click a header to sort. Shift+click to add a secondary sort key.</p>
  </div>
  <table class="tms-table">
    <thead><tr id="tmsHeadRow"></tr></thead>
    <tbody id="tmsBody"></tbody>
  </table>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Ordered multi-key sort state
sortState is an array, not a single field, so priority order is explicit and inspectable.
Short-circuit priority comparator
The sort function only consults a lower-priority key when higher-priority keys tie.
Shift-click to append or toggle
Adds a new secondary/tertiary key, or flips direction if the column is already active.
Plain click resets to single-key
Matches the everyday expectation that a normal click means "just sort by this."
Live numbered priority badges
Each active column shows its real position in sortState, computed on every change.
Per-column direction arrows
Ascending and descending are shown independently for every active sort key.
Type-aware comparator
Numeric and string columns both sort correctly through the same generic compare function.
Stable ties fall through cleanly
Rows identical across all active keys retain their relative original order.

About this UI Snippet

Multi-Column Sort Table — Shift-Click Adds Secondary and Tertiary Sort Keys

Screenshot of the Multi-Column Sort Table snippet rendered live

A single-column sort answers "order by score" — but "order by region, then by level, then by score" needs multiple sort keys applied in priority order, the way spreadsheets handle a multi-level sort dialog. This snippet implements that with shift-click: a plain click sets a column as the sole sort key, while shift-clicking additional columns appends them as secondary, tertiary, and further sort keys, each shown with a small numbered priority badge and its own ascending/descending arrow.

An ordered array of sort keys, not a single field

Where a typical sortable table stores one { key, dir } pair, this one stores an *array* of them, sortState, ordered by priority — index 0 is the primary key, index 1 the secondary, and so on. That ordering is the whole feature: the comparator walks the array in sequence and only moves to the next key when the current one produces a tie, which is exactly how a multi-level spreadsheet sort behaves.

The comparator short-circuits on the first real difference

sortedPlayers()'s compare function loops through sortState in priority order, computing compareValues for each active key and returning immediately once one produces a non-zero result. Only when two rows are completely equal on the primary key does the secondary key ever get consulted, and so on down the list — the standard definition of a stable, priority-ordered multi-key sort.

Plain click resets, shift-click appends or toggles

A plain click on a header collapses sortState down to just that one column (toggling its direction if it was already the sole active sort), matching the everyday expectation that clicking a header means "sort by just this now." Shift-click instead either appends the clicked column to the end of sortState if it isn't already active, or toggles its existing direction in place if it is — so shift-clicking the same secondary column twice flips it between ascending and descending without disturbing its priority position or any other active key.

Numbered badges make priority visible, not just implied

Each active column's header shows a small circular badge with its position in sortState (1, 2, 3…) next to a direction arrow — without this, a user has no way to tell that region is being sorted before level, or that a third key even exists. The badge number is computed live from sortState.findIndex, so it always matches the column's actual current priority, updating immediately if a column is added, removed, or reordered.

A generic comparator across types

compareValues branches on each column's declared type — numeric subtraction for number columns, localeCompare for string columns — so the same multi-key loop works uniformly whether the active keys are a mix of text and numeric fields, without special-casing any particular column in the sort logic itself.

Customizing it

Add a way to remove a key from the middle of sortState (e.g. Ctrl+click), persist the sort configuration to the URL, or combine with a filterable table so multi-sort applies to a filtered subset. Compare with a plain sortable table for the single-key version this builds on.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA player leaderboard renders unsorted, with sortable column headers.
  2. 2
    Click a headerThe table sorts by that column alone; clicking it again reverses direction.
  3. 3
    Shift+click a second headerThat column is added as a secondary sort key, shown with a "2" badge and its own arrow.
  4. 4
    Shift+click a third headerA tertiary key is added with a "3" badge; rows now sort by all three keys in priority order.
  5. 5
    Shift+click an active secondary key againIts direction toggles between ascending and descending without changing its priority.
  6. 6
    Click any header without ShiftAll other sort keys are cleared and that column becomes the sole primary sort.

Real-world uses

Common Use Cases

Leaderboards and rankings
Sort by region, then level, then score, mirroring how ranking systems are actually read.
Reporting and analytics tables
Order results by category and then by a metric, pair with a filterable table.
Spreadsheet-style admin tools
Bring a multi-level sort dialog's behavior directly into a web table's headers.
Scheduling and roster tables
Sort by team, then by shift, then by name for a readable multi-key roster order.
Inventory and catalog browsing
Sort by category, then stock status, then price for merchandising views.
Learning comparator composition
A clear reference for chaining tie-breaking comparators, versus a single-key sortable table.

Got questions?

Frequently Asked Questions

A single object can only represent one active sort column. Multi-level sorting needs an ordered sequence of keys where earlier entries take priority over later ones — an array preserves that order explicitly, and the comparator walks it in sequence, which a single object has no way to express.

It loops through sortState in order, computing each key's comparison result. The function returns as soon as any key produces a non-zero (non-tied) result. Only if the primary key's comparison is exactly 0 — meaning the two rows are equal on that field — does the loop proceed to check the next key in the array.

Add a modifier (e.g. Ctrl+click, or a small × on the badge) that calls sortState.splice(idx, 1) for that column's index, then re-run updateHeaderIndicators() and renderBody() — the remaining keys keep their relative order and their badge numbers recompute automatically since they're derived from array position.

Serialize sortState to a query parameter, e.g. ?sort=region.asc,level.asc,score.desc, on every applySort call, and on page load parse that parameter back into the same array shape before the first renderBody() — the sort logic itself doesn't need to change, only where sortState's initial value comes from.

Keep sortState as an array in component state and derive the sorted rows with a memoized computation (useMemo, computed, or a getter) that re-runs the same short-circuit comparator whenever sortState or the source data changes; bind each header's click handler to call your state setter with the updated array using the same additive/reset logic.