← Template Browser

Sortable Table

Click any column header to sort. Click again to reverse. Add data-numeric to headers whose columns contain numbers.

Live Demo

Name Department Salary Start Date
Alice Chen Engineering $95,000 2022-03-14
Bob Martinez Design $82,000 2021-07-01
Carol Singh Marketing $74,000 2023-01-09
David Kim Engineering $105,000 2020-11-22
Eve Johnson Operations $68,000 2023-06-30
Frank Okonkwo Design $88,000 2022-09-05

Template Code

Copy templates/sortable_table.stpl into your app.stpl. Replace the sample rows list with your own DB query. Move the <style> block into your stylesheet.

{#
================================================================
 Sortable Table  (ScribeFramework template prefab)

 How to use:
   1. Copy this route into your app.stpl
   2. Move the <style> block into your stylesheet (or keep it inline)
   3. Replace the sample `rows` list with your own DB query
   4. Update column headers and <td> cells to match your data
   5. Add data-numeric to any <th> whose column contains numbers

 No external dependencies — vanilla JS only.
================================================================
#}

@route('/example-table')
@no_layout
{$
page_title = "Sortable Table"

# Replace with your own database query, e.g.:
# rows = db['default'].query("SELECT name, department, salary, start_date FROM employees")
rows = [
    {'name': 'Alice Chen',    'department': 'Engineering', 'salary': 95000,  'start_date': '2022-03-14'},
    {'name': 'Bob Martinez',  'department': 'Design',      'salary': 82000,  'start_date': '2021-07-01'},
    {'name': 'Carol Singh',   'department': 'Marketing',   'salary': 74000,  'start_date': '2023-01-09'},
    {'name': 'David Kim',     'department': 'Engineering', 'salary': 105000, 'start_date': '2020-11-22'},
    {'name': 'Eve Johnson',   'department': 'Operations',  'salary': 68000,  'start_date': '2023-06-30'},
    {'name': 'Frank Okonkwo', 'department': 'Design',      'salary': 88000,  'start_date': '2022-09-05'},
]
$}

<style>
/* ── Sortable Table ── move this block to your stylesheet ───── */
.sort-table {
    width: 100%;
    border-collapse: collapse;
}
.sort-table th,
.sort-table td {
    padding: 0.75rem 1rem;
    text-align: left;
    border-bottom: 1px solid var(--border-color, #e2e8f0);
}
.sort-table thead th {
    background: var(--bg-color, #f8fafc);
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
}
.sort-table thead th:hover {
    color: var(--primary-color, #2563eb);
}
.sort-table tbody tr:hover {
    background: var(--primary-light, #dbeafe);
}
.sort-icon {
    display: inline-block;
    margin-left: 0.25rem;
    color: var(--text-muted, #64748b);
    font-size: 0.8em;
}
th.sort-asc .sort-icon,
th.sort-desc .sort-icon {
    color: var(--primary-color, #2563eb);
}
/* ──────────────────────────────────────────────────────────── */
</style>

<div class="container">
    <h1>Employees</h1>
    <div class="card" style="overflow-x: auto; padding: 0;">
        <table id="employee-table" class="sort-table">
            <thead>
                <tr>
                    <th data-col="0">Name <span class="sort-icon">↕</span></th>
                    <th data-col="1">Department <span class="sort-icon">↕</span></th>
                    <th data-col="2" data-numeric>Salary <span class="sort-icon">↕</span></th>
                    <th data-col="3">Start Date <span class="sort-icon">↕</span></th>
                </tr>
            </thead>
            <tbody>
                {% for row in rows %}
                <tr>
                    <td>{{ row.name }}</td>
                    <td>{{ row.department }}</td>
                    <td>${{ "{:,}".format(row.salary) }}</td>
                    <td>{{ row.start_date }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>
</div>

<script>
(function () {
    const table = document.getElementById('employee-table');
    let sortCol = -1;
    let sortAsc = true;

    table.querySelectorAll('thead th').forEach(th => {
        th.addEventListener('click', () => {
            const col     = parseInt(th.dataset.col);
            const numeric = 'numeric' in th.dataset;

            sortAsc = (sortCol === col) ? !sortAsc : true;
            sortCol = col;

            // Update header indicators
            table.querySelectorAll('thead th').forEach(h => {
                h.classList.remove('sort-asc', 'sort-desc');
                h.querySelector('.sort-icon').textContent = '↕';
            });
            th.classList.add(sortAsc ? 'sort-asc' : 'sort-desc');
            th.querySelector('.sort-icon').textContent = sortAsc ? '↑' : '↓';

            // Sort rows
            const tbody = table.querySelector('tbody');
            Array.from(tbody.querySelectorAll('tr'))
                .sort((a, b) => {
                    const av = a.cells[col].textContent.trim();
                    const bv = b.cells[col].textContent.trim();
                    const cmp = numeric
                        ? parseFloat(av.replace(/[^0-9.]/g, '')) - parseFloat(bv.replace(/[^0-9.]/g, ''))
                        : av.localeCompare(bv);
                    return sortAsc ? cmp : -cmp;
                })
                .forEach(row => tbody.appendChild(row));
        });
    });
})();
</script>