A web component to enable users to sort table data by clicking on column headers. Built with accessibility in mind, featuring keyboard navigation, screen reader announcements, and light DOM manipulation.
Based on the original jquery.easy-sortable-tables.js by Aaron Gustafson.
tbody elements when sortingnpm install @aarongustafson/table-sortable
TypeScript ready
The package ships with
table-sortable.d.ts. Importing either the class ({ TableSortableElement }) or the guarded definition helper automatically provides typed event payloads (table-sortable:sort) and reflective properties (labelSortable,labelAscending,labelDescending).
Import the class and define the custom element with your preferred tag name:
import { TableSortableElement } from '@aarongustafson/table-sortable';
customElements.define('my-custom-name', TableSortableElement);
Use the guarded definition helper to register the element when customElements is available:
import '@aarongustafson/table-sortable/define.js';
If you prefer to control when the element is registered, call the helper directly:
import { defineTableSortable } from '@aarongustafson/table-sortable/define.js';
defineTableSortable();
You can also include the guarded script from HTML:
<script src="./node_modules/@aarongustafson/table-sortable/define.js" type="module"></script>
Wrap your existing table in a <table-sortable> element. The component uses progressive enhancement - it will automatically inject sortable buttons into table headers:
<table-sortable>
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
<tbody>
<tr>
<td>Charlie</td>
<td>35</td>
<td>New York</td>
</tr>
<tr>
<td>Alice</td>
<td>28</td>
<td>Boston</td>
</tr>
<tr>
<td>Bob</td>
<td>42</td>
<td>Chicago</td>
</tr>
</tbody>
</table>
</table-sortable>
The component uses pure progressive enhancement - it automatically creates accessible <button> elements inside each <th> for sorting. You don't need to add any buttons or links manually.
Use the data-sort-value attribute to specify a custom value to sort by, different from the displayed content:
<table-sortable>
<table>
<thead>
<tr>
<th>Product</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr>
<td data-sort-value="WIDGET-B">Widget B (Premium)</td>
<td data-sort-value="50">$50.00</td>
</tr>
<tr>
<td data-sort-value="WIDGET-A">Widget A (Basic)</td>
<td data-sort-value="30">$30.00</td>
</tr>
</tbody>
</table>
</table-sortable>
This is useful for:
Use an element with the class [data-sort-as] to provide a hidden sort value:
<table-sortable>
<table>
<thead>
<tr>
<th>Full Name</th>
</tr>
</thead>
<tbody>
<tr>
<td><span data-sort-as>SMITH</span>John Smith</td>
</tr>
<tr>
<td><span data-sort-as>ANDERSON</span>Emily Anderson</td>
</tr>
</tbody>
</table>
</table-sortable>
The [data-sort-as] element should be hidden with CSS:
[data-sort-as] {
display: none;
}
This allows sorting by last name while displaying "First Last" format.
To maintain groupings while sorting, use multiple <tbody> elements with data-table-sort-group attributes:
<table-sortable>
<table>
<thead>
<tr>
<th>Name</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr data-table-sort-group="engineering">
<td>Charlie (Engineering)</td>
<td>85</td>
</tr>
</tbody>
<tbody>
<tr data-table-sort-group="design">
<td>Alice (Design)</td>
<td>92</td>
</tr>
</tbody>
<tbody>
<tr data-table-sort-group="sales">
<td>Bob (Sales)</td>
<td>78</td>
</tr>
</tbody>
</table>
</table-sortable>
Each group (tbody) will be sorted independently and maintain its structure.
The component fires custom events that you can listen to:
| Event | Description | Detail |
|---|---|---|
table-sortable:sort |
Fired when a column is sorted | { column: number, direction: 'asc'|'desc', header: HTMLElement } |
const element = document.querySelector('table-sortable');
element.addEventListener('table-sortable:sort', (event) => {
const { column, direction, header } = event.detail;
console.log(`Sorted column ${column} (${header.textContent}) in ${direction}ending order`);
});
The component supports attributes for customizing screen reader announcements:
| Attribute | Description | Default |
|---|---|---|
label-sortable |
Label for unsorted columns | "Click to sort" |
label-ascending |
Label when sorted ascending | "sorted ascending. Click to sort descending" |
label-descending |
Label when sorted descending | "sorted descending. Click to sort ascending" |
<table-sortable
label-sortable="Cliquez pour trier"
label-ascending="trié croissant. Cliquez pour trier décroissant"
label-descending="trié décroissant. Cliquez pour trier croissant">
<table>
<!-- table content -->
</table>
</table-sortable>
The component uses light DOM, so you can style the table normally with CSS. The component adds these classes to help style sort indicators:
| Class | Applied To | Description |
|---|---|---|
active |
<th> |
The currently sorted column |
up |
<th> |
Column sorted in ascending order |
down |
<th> |
Column sorted in descending order |
sorted |
<col> |
The currently sorted column (for column styling) |
The component supports CSS custom properties for visual indicators:
| Property | Description | Default |
|----------|-------------|---------||
| --table-sortable-indicator-asc | Ascending sort indicator | ↑ |
| --table-sortable-indicator-desc | Descending sort indicator | ↓ |
/* Customize sort indicators */
table-sortable {
--table-sortable-indicator-asc: '▲';
--table-sortable-indicator-desc: '▼';
}
/* Style active column header */
thead th.active {
background-color: #e3f2fd;
}
/* Highlight sorted column */
col.sorted {
background-color: rgba(0, 102, 204, 0.05);
}
The component is built with accessibility as a priority:
Keyboard Navigation:
Enter or Space keysScreen Reader Support:
aria-sort attribute indicates column sort state (ascending, descending, none)Progressive Enhancement:
Visual Indicators:
<col> elementsThe component automatically injects <colgroup> and <col> elements if they don't exist, allowing you to style entire columns. The currently sorted column receives a .sorted class on its corresponding <col> element.
The component automatically detects numeric values and sorts them numerically rather than alphabetically (so 100 comes after 20, not after 1).
This component uses modern web standards:
Supported Browsers:
For older browsers, you may need polyfills from @webcomponents/webcomponentsjs.
# Install dependencies
npm install
# Run tests
npm test
# Run tests with UI
npm run test:ui
# Run tests with coverage
npm run test:coverage
# Lint code
npm run lint
# Format code
npm run format
# View demo locally
open demo/index.html
MIT © Aaron Gustafson
Based on jquery.easy-sortable-tables.js © Aaron Gustafson