JSFiddle - React, Tailwind, and code Playground
by Imri Paloja
HTML
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"
/>
<div class="container">
<table class="sortable">
<thead>
<tr>
<th onclick="sortTable('sortable', 0)">Name</th>
<th onclick="sortTable('sortable', 1)">Age</th>
<th onclick="sortTable('sortable', 2)">Score</th>
<th onclick="sortTable('sortable', 3)">Salary</th>
<th onclick="sortTable('sortable', 4)">Progress</th>
<th onclick="sortTable('sortable', 5)">Status</th>
<th onclick="sortTable('sortable', 6)">Last Update</th>
</tr>
</thead>
<tbody>
<tr>
<td>Emma Thompson</td>
<td>28</td>
<td>92.5</td>
<td>$4,850</td>
<td>87%</td>
<td>Active</td>
<td>12/03/2025</td>
</tr>
<tr>
<td>Lucas Bennett</td>
<td>41</td>
<td>78</td>
<td>6200</td>
<td>62%</td>
<td>Active</td>
<td>08/11/2025</td>
</tr>
<tr>
<td>Sofia Rodriguez</td>
<td>19</td>
<td>95.75</td>
<td>$2,300</td>
<td>98%</td>
<td>Training</td>
<td>03/01/2026</td>
</tr>
<tr>
<td>James Chen</td>
<td>34</td>
<td>64.2</td>
<td>€5 450</td>
<td>45%</td>
<td>On Leave</td>
<td>29/10/2025</td>
</tr>
<tr>
<td>Ava Martinez</td>
<td>-</td>
<td>88</td>
<td>$7,120</td>
<td>91%</td>
<td>Active</td>
<td>15/12/2025</td>
</tr>
<tr>
<td>Noah Kim</td>
<td>37</td>
<td>45.5</td>
<td>-</td>
<td>31%</td>
<td>Inactive</td>
<td>04/09/2025</td>
</tr>
<tr>
...
CSS
th.sort-asc::after {
content: " ↑";
color: #0066cc;
}
th.sort-desc::after {
content: " ↓";
color: #0066cc;
}
th {
cursor: pointer;
user-select: none;
}
.container {
margin-top: 5%;
}
html,
body {
color: #454545;
}
table {
width: 100%;
}
JavaScript
/**
* Sort HTML table by column index
* @param {string} tableClass - class name of the table
* @param {number} colIndex - column index to sort by (0-based)
*/
function sortTable(tableClass, colIndex) {
const table = document.querySelector(`.${tableClass}`);
if (!table) return;
const tbody = table.tBodies[0];
const rows = Array.from(tbody.rows);
// Get current sort direction from data attribute (or default to asc)
const header = table.querySelectorAll('th')[colIndex];
const currentDir = header?.dataset.sortDir || 'asc';
const direction = currentDir === 'asc' ? 'desc' : 'asc';
// Clean previous sort indicators
table.querySelectorAll('th').forEach(th => {
th.dataset.sortDir = '';
th.classList.remove('sort-asc', 'sort-desc');
});
// Set new direction
if (header) {
header.dataset.sortDir = direction;
header.classList.add(direction === 'asc' ? 'sort-asc' : 'sort-desc');
}
// Natural compare function (handles numbers, strings, dashes, etc.)
rows.sort((a, b) => {
let x = getCellValue(a, colIndex);
let y = getCellValue(b, colIndex);
// Handle special dash case
if (x === '-' && y !== '-') return direction === 'asc' ? -1 : 1;
if (y === '-' && x !== '-') return direction === 'asc' ? 1 : -1;
if (x === '-' && y === '-') return 0;
// Natural compare
return naturalCompare(x, y) * (direction === 'asc' ? 1 : -1);
});
// Re-attach rows in new order (very efficient)
rows.forEach(row => tbody.appendChild(row));
}
// Extract clean value from cell
function getCellValue(row, index) {
const cell = row.cells[index];
if (!cell) return '';
let text = cell.textContent || cell.innerText || '';
// Remove extra spaces
text = text.trim();
// Handle common special cases
if (text === '-' || text === '—' || text === '–') return...