Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

To sort an HTML table when a reader clicks a column heading, collect the rows from its <tbody>, compare each row’s value in that column, then append the rows back in sorted order. The example below adds ascending and descending sorting for text and numeric columns, with keyboard-operable buttons and an accessible sort state.

Set up the table with sortable headers

Keep column headings in <thead> and data rows in <tbody>. Put a real button inside each sortable header so it can be activated with a keyboard as well as a pointer.

<table>
  <caption>Scores. Activate a column heading to sort the table.</caption>
  <thead>
    <tr>
      <th scope="col"><button type="button" data-sort="name">Name</button></th>
      <th scope="col"><button type="button" data-sort="score">Score</button></th>
    </tr>
  </thead>
  <tbody>
    <tr><td>Alex</td><td>15</td></tr>
    <tr><td>Devlin</td><td>12</td></tr>
    <tr><td>Sam</td><td>18</td></tr>
  </tbody>
</table>

The data-sort values identify each column’s comparison type in the script. Keep them aligned with the comparison logic you add. The sample expects one simple header row, with header positions matching data-cell positions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Add the sorting behavior

Place this script after the table or run it after the document has loaded. It uses a regular array of the body’s rows, sorts that array, and then moves the existing row elements into the new order.

const table = document.querySelector("table");
const tbody = table.tBodies[0];
const buttons = table.querySelectorAll("thead button[data-sort]");
let activeColumn = null;
let ascending = true;

for (const button of buttons) {
  button.addEventListener("click", () => {
    const key = button.dataset.sort;
    const header = button.closest("th");
    const columnIndex = Array.from(header.parentElement.cells).indexOf(header);
    const rows = Array.from(tbody.rows);

    if (activeColumn === key) {
      ascending = !ascending;
    } else {
      activeColumn = key;
      ascending = true;
    }

    rows.sort((rowA, rowB) => {
      const a = rowA.cells[columnIndex].textContent.trim();
      const b = rowB.cells[columnIndex].textContent.trim();
      const result = key === "score"
        ? Number(a) - Number(b)
        : a.localeCompare(b);

      return ascending ? result : -result;
    });

    tbody.append(...rows);

    for (const cell of table.tHead.rows[0].cells) {
      cell.removeAttribute("aria-sort");
    }
    header.setAttribute("aria-sort", ascending ? "ascending" : "descending");
  });
}
  1. Find the table elements. table.tBodies[0] selects the first body section, and the query selects the header buttons.
  2. Get the column position. The script finds the clicked button’s header cell, then gets that cell’s position in its header row.
  3. Choose the direction. Clicking the active column reverses its direction; selecting a different column starts ascending.
  4. Compare and reorder. The comparison uses numeric subtraction for scores and localeCompare() for names. tbody.append(...rows) moves the sorted row elements into their new positions.
  5. Expose the current state. The active header receives aria-sort; the other headers have the attribute removed.

To show direction visually, add an arrow or other indicator to the active button and update it in the click handler. The visual cue should agree with the aria-sort value.

Choose a comparator that matches the data

JavaScript’s Array.prototype.sort() sorts an array in place. Without a comparator, it converts values to strings and orders them lexically; for example, numbers such as 2 and 10 will not sort in numeric order. Use a comparator that returns a negative value when the first item comes before the second, a positive value when it comes after, and zero when they compare equal. MDN explains the method’s behavior and comparator requirements in its Array.prototype.sort() reference.

  • Text: a.localeCompare(b) is a straightforward option for comparing text.
  • Numbers: Compare numeric values with subtraction, as in Number(a) - Number(b), only when the cell text is a plain number.
  • Formatted or missing values: Normalize the underlying value before comparison. Handle blanks and invalid values explicitly rather than relying on accidental conversions.
  • Dates: Compare a normalized date value, not a display string whose format may sort differently from chronological order.

A comparator should apply consistent ordering rules. Returning only 1 and 0, for example, fails to distinguish both directions of a comparison and can produce inconsistent results. Since ECMAScript 2019, the specification requires stable sorting: rows that compare equal retain their prior relative order. MDN notes that the specification does not guarantee a particular time or space complexity for sort().

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Make the sort state accessible

Use semantic table markup, including <th scope="col">, and make each sortable heading a button. The button communicates that the heading is an interactive control; aria-sort="ascending" or aria-sort="descending" on the currently sorted header communicates the direction to assistive technology. Keep the attribute on only the active heading, and use a visible direction indicator as well. See the W3C WAI-ARIA APG sortable-table example and MDN’s guidance on aria-sort.

The table caption in the example explains that headings can be activated to sort. Test the interaction with keyboard navigation and assistive technology; a visual arrow alone does not communicate the state to every user.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Adapt the example to your table

This compact script is suitable for a simple table with one header row and plain text or numeric cells. Adjust the approach when the table has richer structure or data:

  • Blank, invalid, or localized values: Decide where these belong and parse them according to the format before comparing. Number() does not automatically interpret every locale’s decimal or thousands separators.
  • Links or nested markup in cells: textContent reads the text, but it may not be the value you intend to sort. Store a normalized value in a data attribute or use the underlying data instead.
  • Multiple body sections: The example sorts only the first <tbody>. Decide whether each group should sort independently or whether the data model should be sorted before rendering.
  • Spanning or multi-row headers: A header’s position may not match its data column when cells use colspan or rowspan. Use an explicit mapping from each button to its data column.
  • Server-paginated or large data sets: Sorting the rows currently in the DOM only reorders those rows. If other records are on another page or held by a server, sort the underlying complete data set or request a sorted page from the data source.

MDN’s HTML table-row reference also demonstrates click-driven row sorting; its simple example assumes plain text and the first body section, so more complex tables need an adapted column mapping and data strategy.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.