Day 16: Sortable Table
Build a sortable table where clicking a column header reorders the rows by that column and toggles between ascending and descending sort order.
JavaScript focus
- selecting table rows
- converting rows into an array
- reading cell values
- comparing strings and numbers
- sorting arrays
- toggling state for ascending and descending order
- appending reordered rows back into the table body
Nice extras
- add a visual indicator for active sort direction
- support both text and numeric columns
- keep one default sorted column on page load
- use buttons inside th elements for better accessibility
- update aria-sort on the active header
- reset other headers when a new column is sorted
- keep the sort logic reusable so you can apply it to other tables later
MDN prep
Sortable Table
| Wireless Mouse | Accessories | $29.99 | 120 |
|---|---|---|---|
| Mechanical Keyboard | Accessories | $89.99 | 45 |
| USB-C Hub | Accessories | $49.50 | 78 |
| 4K Monitor | Displays | $399.99 | 12 |
| Laptop Stand | Office | $34.25 | 64 |
| Noise Cancelling Headphones | Audio | $199.99 | 23 |
| Webcam | Video | $79.00 | 39 |
| Portable SSD 1TB | Storage | $129.99 | 56 |
| Ergonomic Chair | Office | $299.00 | 8 |
| Desk Lamp | Office | $22.50 | 91 |
The HTML
<table class="table" id="sortable_table">
<caption class="sr-only">Product inventory with sortable columns for name, category, price, and stock</caption>
<thead>
<tr>
<th scope="col">
<button type="button" class="sort_button" data-column="0" aria-sort="none">Product Name</button>
</th>
<th scope="col">
<button type="button" class="sort_button" data-column="1" aria-sort="none">Category</button>
</th>
<th scope="col">
<button type="button" class="sort_button" data-column="2" aria-sort="none">Price</button>
</th>
<th scope="col">
<button type="button" class="sort_button" data-column="3" aria-sort="none">Stock</button>
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Wireless Mouse</th>
<td>Accessories</td>
<td data-value="29.99">$29.99</td>
<td data-value="120">120</td>
</tr>
<tr>
<th scope="row">Mechanical Keyboard</th>
<td>Accessories</td>
<td data-value="89.99">$89.99</td>
<td data-value="45">45</td>
</tr>
<tr>
<th scope="row">USB-C Hub</th>
<td>Accessories</td>
<td data-value="49.5">$49.50</td>
<td data-value="78">78</td>
</tr>
<tr>
<th scope="row">4K Monitor</th>
<td>Displays</td>
<td data-value="399.99">$399.99</td>
<td data-value="12">12</td>
</tr>
<tr>
<th scope="row">Laptop Stand</th>
<td>Office</td>
<td data-value="34.25">$34.25</td>
<td data-value="64">64</td>
</tr>
<tr>
<th scope="row">Noise Cancelling Headphones</th>
<td>Audio</td>
<td data-value="199.99">$199.99</td>
<td data-value="23">23</td>
</tr>
<tr>
<th scope="row">Webcam</th>
<td>Video</td>
<td data-value="79">$79.00</td>
<td data-value="39">39</td>
</tr>
<tr>
<th scope="row">Portable SSD 1TB</th>
<td>Storage</td>
<td data-value="129.99">$129.99</td>
<td data-value="56">56</td>
</tr>
<tr>
<th scope="row">Ergonomic Chair</th>
<td>Office</td>
<td data-value="299">$299.00</td>
<td data-value="8">8</td>
</tr>
<tr>
<th scope="row">Desk Lamp</th>
<td>Office</td>
<td data-value="22.5">$22.50</td>
<td data-value="91">91</td>
</tr>
</tbody>
</table>
The JavaScript
function initSortableTable() {
const root = document.querySelector("#sortable_table");
if (!root) return;
const tbody = root.querySelector("tbody");
if (!tbody) return;
// get all the buttons (node list)
const buttons = root.querySelectorAll(".sort_button");
// loop through each button
buttons.forEach((button) => {
// when clicking on a button...
button.addEventListener("click", () => {
// get all rows (node list) + make an array
const rowsArray = Array.from(tbody.querySelectorAll("tr"));
// get the column number from the data-* attribute
const columnIndex = Number(button.dataset.column);
// get the aria-sort attribute + check the status
const currentSort = button.getAttribute("aria-sort");
const nextSort = currentSort === "ascending" ? "descending" : "ascending";
// reset aria-sort=none
buttons.forEach((btn) => {
btn.setAttribute("aria-sort", "none");
});
// sort the rows arrays asc or desc
rowsArray.sort((a, b) => {
// get the children of the rows
const cellA = a.children[columnIndex];
const cellB = b.children[columnIndex];
// check the children for either a data-* (number) or text
const valueA = cellA.dataset.value ? cellA.dataset.value : cellA.textContent.trim();
const valueB = cellB.dataset.value ? cellB.dataset.value : cellB.textContent.trim();
// check the aria-sort + sort by number accordingly
if (cellA.dataset.value) {
return nextSort === "ascending" ? Number(valueA) - Number(valueB) : Number(valueB) - Number(valueA);
}
// check the text and sort a-z or z-a
return nextSort === "ascending" ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
});
// append the rows by sort order
rowsArray.forEach((row) => {
tbody.append(row);
});
// set the aria-sort to the new sort value
button.setAttribute("aria-sort", nextSort);
});
});
}
initSortableTable();