Bootstrap Data Table with Sortable Columns — Free Snippet
Bootstrap Data Table with Sortable Columns · Tables · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="container py-5">
<table class="table table-hover bssort-table">
<thead>
<tr>
<th class="bssort-col" data-key="name" data-type="text">Customer <span class="bssort-arrow"></span></th>
<th class="bssort-col" data-key="orders" data-type="num">Orders <span class="bssort-arrow"></span></th>
<th class="bssort-col" data-key="total" data-type="num">Total spent <span class="bssort-arrow"></span></th>
<th class="bssort-col" data-key="joined" data-type="date">Joined <span class="bssort-arrow"></span></th>
</tr>
</thead>
<tbody id="bssortBody">
<tr data-name="Ada Lovelace" data-orders="12" data-total="1240" data-joined="2024-03-14"><td>Ada Lovelace</td><td>12</td><td>$1,240</td><td>2024-03-14</td></tr>
<tr data-name="Grace Hopper" data-orders="4" data-total="380" data-joined="2025-01-02"><td>Grace Hopper</td><td>4</td><td>$380</td><td>2025-01-02</td></tr>
<tr data-name="Alan Turing" data-orders="27" data-total="3120" data-joined="2023-08-30"><td>Alan Turing</td><td>27</td><td>$3,120</td><td>2023-08-30</td></tr>
<tr data-name="Katherine Johnson" data-orders="9" data-total="905" data-joined="2024-11-19"><td>Katherine Johnson</td><td>9</td><td>$905</td><td>2024-11-19</td></tr>
</tbody>
</table>
</div>.bssort-col { cursor: pointer; user-select: none; }
.bssort-col:hover { color: #6366f1; }
.bssort-arrow { font-size: 10px; opacity: .4; }
.bssort-col.bssort-active .bssort-arrow { opacity: 1; color: #6366f1; }const headers = document.querySelectorAll('.bssort-col');
const body = document.getElementById('bssortBody');
let sortKey = null;
let ascending = true;
const GETTERS = {
text: row => row.dataset.name.toLowerCase(),
num: (row, key) => Number(row.dataset[key]),
date: row => new Date(row.dataset.joined).getTime(),
};
headers.forEach(th => {
th.addEventListener('click', () => {
const key = th.dataset.key;
ascending = sortKey === key ? !ascending : true;
sortKey = key;
headers.forEach(h => { h.classList.remove('bssort-active'); h.querySelector('.bssort-arrow').textContent = ''; });
th.classList.add('bssort-active');
th.querySelector('.bssort-arrow').textContent = ascending ? '▲' : '▼';
const type = th.dataset.type;
const rows = Array.from(body.children);
rows.sort((a, b) => {
const va = GETTERS[type](a, key);
const vb = GETTERS[type](b, key);
const cmp = va < vb ? -1 : va > vb ? 1 : 0;
return ascending ? cmp : -cmp;
});
rows.forEach(row => body.appendChild(row));
});
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Bootstrap Data Table with Sortable Columns — HTML, CSS & JavaScript

Every column header in this table has a data-type of text, num, or date — the detail that makes the sort actually correct rather than a naive string comparison. Sorting the "Total spent" column as plain text would put $3,120 before $380 (since "3" sorts before "9" in "9" vs "3..."-style string comparison isn't even the issue here — the real trap is comparing formatted currency strings at all). This snippet instead reads each row's raw, unformatted value from a data attribute (data-total="3120") and a per-type GETTERS function converts it correctly — Number() for numeric columns, new Date().getTime() for the date column — before comparing, all built on real Bootstrap 5.3 table markup.
Clicking the same header twice flips between ascending and descending, tracked with one shared ascending boolean reset to true whenever a *different* column is clicked — so switching columns always starts from a predictable ascending sort rather than remembering a stale direction from whichever column was sorted last.
Step by step
How to Use
- 1Load the snippetClick the snippet in the sidebar Library tab. The preview loads a 4-row customer table in its original order.
- 2Click "Total spent"Rows sort ascending by spend, with an up arrow on the header confirming the direction.
- 3Click "Total spent" againThe sort reverses to descending — highest spender first.
- 4Click "Joined"Sorts correctly by actual date order, not by the displayed text string.
- 5Add a fifth rowAdd a <tr> with matching data-name/orders/total/joined attributes — it sorts correctly with the rest automatically.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Visible text like "$1,240" or "2024-03-14" doesn't sort correctly as a plain string — currency symbols and formatting break numeric ordering, and date strings only sort correctly by coincidence in the ISO format used here. Each row instead carries its raw, unformatted value in a data attribute, which is what actually gets compared.
An ascending boolean flips every time the same column's key is clicked again; clicking a different column resets it back to true (ascending), so direction never carries over unexpectedly between columns.
It reorders the real DOM elements — body.appendChild(row) on a row already in the table moves it to the new position, so the genuine row order (and reading/tab order) changes, not just a visual rearrangement.
Add a new <th> with a data-key, a data-type (text/num/date), and a matching data attribute on every row holding that column's raw value — the click handler and GETTERS object already handle any column following this convention.
Yes — add a new getter function to the GETTERS object (e.g. mapping status strings to a priority number) and reference it via a new data-type value on that column's header.