Sortable Table — Free HTML CSS JS Snippet

Sortable Table · Tables · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

sortBy(th) reads data-col index and data-type to select string or numeric comparison
Array.sort() with re-append DOM pattern — no virtual DOM, no library needed
data-val attribute stores raw numbers for currency/unit columns — prevents NaN comparison
.asc/.desc classes toggle arrow indicator via CSS ::after content property
All headers reset before setting active class — single-column sort guaranteed
Active column: indigo background on th.asc and th.desc for visual identification
Neutral ↕, ascending ↑, descending ↓ arrows via CSS content — no innerHTML changes
Export as HTML file, React JSX component, or React + Tailwind CSS
Live split-pane editor — preview updates as you type
Mobile (375px), Tablet (768px), Desktop device preview buttons

About this UI Snippet

Sortable Table — Click Column Header Sort, asc/desc Toggle & data-val Numeric Comparison

Screenshot of the Sortable Table snippet rendered live

Column sorting is one of the most-requested features on any data table. When users can click a header to sort by price (lowest first), date (newest first), or name (A to Z), they find what they need without scrolling through unsorted data. This snippet provides a complete sortable HTML CSS JavaScript table with ascending/descending toggle, an arrow direction indicator, and a data-val attribute pattern for correct numeric sorting on formatted values like currency and units.

How the sort function works

sortBy(th) receives the clicked th element. It reads th.dataset.col to know which column index to sort, and th.dataset.type to know whether to sort as a string or number. It checks whether the header already has the .asc class to determine current direction — if it does, the next click will be descending; if not, ascending.

All headers are reset first (remove .asc, .desc, and active background) before the clicked header receives its new class. This ensures only one column is ever marked as active at a time. The rows are spread into an array via [...tbody.querySelectorAll("tr")], sorted using Array.sort(), and then re-appended to the tbody in sorted order.

Why data-val is necessary for numeric columns

A cell displaying "$89.99" cannot be compared as a number directly — the dollar sign makes parseFloat return NaN. The data-val attribute stores the raw numeric value separately: data-val="89.99" on the cell showing "$89.99". The sort function reads parseFloat(cell.dataset.val) for numeric columns. This technique applies to any column with formatted values: prices with currency symbols, quantities with units ("142 units"), percentages with symbols, or dates formatted as strings.

The arrow direction indicator

The .sortable::after pseudo-element uses CSS content to display ↕ (neutral), ↑ (ascending), or ↓ (descending) based on whether the header has .asc or .desc. The active sort column gets an indigo background to visually mark which column is currently sorted. The arrow updates purely in CSS — no innerHTML or attribute changes needed in JavaScript.

String vs number sort

data-type="number" on a th triggers parseFloat comparison: valA - valB for ascending, valB - valA for descending. Any th without this attribute defaults to string comparison: valA.toLowerCase().localeCompare(valB.toLowerCase()). Both handle null/empty cells gracefully.

Combining with search filter

The Sortable Table and Data Table snippets use the same thead/tbody structure. Add the filterTable() function from the Data Table snippet to this table — search and sort operate on the same tbody rows without conflicting. For long datasets, layer in pagination controls so sorted results stay paged.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to trace the comparator logic by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the data-val attribute exists separately from a cell's visible textContent, and what specific comparison would silently break (returning NaN) without it for a column showing formatted currency. The same assistant can help optimize it, for instance checking whether spreading tbody.querySelectorAll('tr') into an array and calling Array.sort() on every click scales fine for a few hundred rows or whether a larger dataset would benefit from sorting a lighter array of row data instead of live DOM nodes. It is just as useful for extending the table: ask it to add Shift-click multi-column secondary sorting, add a data-type of "date" that compares Unix timestamps stored in data-val, or persist the last-sorted column and direction to localStorage so it's restored on the next page load. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a click-to-sort HTML table in plain HTML, CSS, and JavaScript — no library.

Requirements:
- A table where every sortable th carries a data-col attribute (its column index) and an optional data-type attribute set to "number" for numeric columns; columns without data-type must default to string comparison.
- For any column displaying a formatted value that isn't directly parseable as a number (currency with a dollar sign, a quantity with a unit suffix, etc.), the corresponding td must carry a separate data-val attribute holding the raw numeric value, and the sort comparator must read that attribute via parseFloat instead of ever trying to parse the cell's visible textContent for numeric columns.
- Clicking a header must: determine the new sort direction by checking whether that header already has an "ascending" class (toggling to descending if so, ascending otherwise), remove any ascending/descending class from every other header first so only one column is ever marked active, convert the tbody's rows into an array, sort that array with a comparator that branches on the column's data-type, and then re-append the sorted row elements back into the tbody in their new order (not rebuild the rows from scratch).
- The active sorted column's header must visually indicate both that it's the active sort column and which direction, using a CSS ::after pseudo-element whose content changes based on the ascending/descending/neutral class, so no innerHTML manipulation is needed just to update the arrow.
- The sort function must work generically for any number of columns and rows without hardcoding column count, so adding a new sortable column requires only adding a th with the right data attributes and matching td cells.

Step by step

How to Use

  1. 1
    Click any column header to sortClick Product, Category, Price, Stock, or Status header. The column sorts ascending on first click, descending on second. The arrow indicator and indigo background identify the active column.
  2. 2
    Add more sortable columnsAdd a th with class="sortable", data-col="N" (column index), and onclick="sortBy(this)". Add matching td cells. For numeric columns, also add data-type="number".
  3. 3
    Mark currency and formatted columns as numericAdd data-type="number" to any th that contains prices, quantities, or other numeric data. Add data-val="rawNumber" to each corresponding td so the dollar sign or unit label does not break numeric comparison.
  4. 4
    Replace the sample rows with your dataIn the HTML panel, replace the tbody tr rows with your own records. The sort function adapts to any row count and any column count without changes.
  5. 5
    Combine with live searchAdd the filterTable() function from the Data Table snippet. Both functions work on the same tbody rows independently — search by text and sort by column can run simultaneously.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component using useState for sort state, or "Tailwind" for a React + Tailwind CSS version.

Real-world uses

Common Use Cases

Product inventory and catalogue tables
Let users sort products by price (lowest first for budget shoppers), stock level (critical-low first for restock priority), or name (A-Z for browsing). Column sorting is the single most-used interaction on product data tables.
Analytics and business metrics dashboards
Sort metric rows by highest revenue channel, lowest conversion rate (needing attention), or most traffic source. Sorting surfaces actionable insights from flat data tables without needing a separate chart component.
Order management and fulfilment queues
Sort open orders by date (oldest first for FIFO fulfilment), order value (highest first for VIP prioritisation), or delivery deadline. Ascending/descending toggle gives fulfilment teams flexible queue management.
Learn Array.sort() and DOM row reordering
sortBy() spreads tbody rows into an array, calls Array.sort() with a custom comparator, then re-appends rows in sorted order. Studying this function teaches how JavaScript sort works on DOM elements and why the data-val pattern is needed for numeric string data.
Combine with Data Table search for full data grid
The Data Table snippet in this library provides live search via textContent filtering. Both snippets use identical table structure — add filterTable() from that snippet to this one to get search + sort in a single component without any conflict.
Financial statements and reporting tables
Sort transaction tables by amount (largest first for review), date (most recent first for auditing), or category (alphabetical for grouping). The data-val attribute handles any formatted numeric column, including negative values and decimal amounts.

Got questions?

Frequently Asked Questions

Each sortable th has a data-type attribute. data-type="number" triggers parseFloat(cell.dataset.val) comparison, giving correct numeric ordering. Without data-type, the function defaults to cell.textContent.toLowerCase().localeCompare() for alphabetical string comparison. You can add other types (date, boolean) by adding a new branch in the comparison function.

A cell showing "$89.99" or "142 units" has non-numeric characters in its textContent. parseFloat("$89.99") returns NaN, which breaks sorting. data-val="89.99" stores only the raw number separately. The sort function reads parseFloat(cell.dataset.val) for numeric columns, getting accurate float comparison regardless of how the cell is formatted for display.

Add data-type="number" to the date column th. In each date cell, add data-val with a Unix timestamp (seconds or milliseconds since epoch): data-val="1704067200". The numeric sort then orders rows chronologically. If your dates are ISO strings like "2024-01-01", you can also use data-type="string" — ISO format sorts correctly as a string because the year comes first.

Store sort state as an array of objects: let sortKeys = []. On header click, push or replace entries. In the sort comparator, iterate sortKeys in order: compare by primary key first; if equal, compare by secondary key. You can trigger secondary sort with Shift+Click by checking e.shiftKey in the sortBy function.

At the bottom of your script, call sortBy(document.querySelector(".sortable[data-col='2']")) — replacing 2 with your target column index. This runs the sort function exactly as if the user had clicked that column header, applying the default ascending sort and updating the arrow indicator.

Click "JSX" to download. In React, manage sortCol (number) and sortDir ("asc" or "desc") in useState. Derive sortedRows using [...rows].sort() based on current sort state — use useMemo to avoid re-sorting on every render. Apply the active column class via a conditional className on each th. The data-val pattern becomes a numeric property on the row data object.