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...