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.
ariaNotify() or a live-region fallbacknpm 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:
data-sort-asUse a [data-sort-as] element to mark the visible text fragment that should lead sorting:
<table-sortable>
<table>
<thead>
<tr>
<th>Full Name</th>
</tr>
</thead>
<tbody>
<tr>
<td>John <span data-sort-as>Smith</span></td>
</tr>
<tr>
<td>Emily <span data-sort-as>Anderson</span></td>
</tr>
</tbody>
</table>
</table-sortable>
This allows sorting by last name while still displaying the full name naturally.
If you need a sort token that should not be visible, you can still hide a [data-sort-as] element with CSS, but visible text is generally preferable to reduce duplicated content.
To keep related rows together while sorting, add the same
data-table-sort-group value to every <tr> in a group:
<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>
<tr data-table-sort-group="engineering">
<td>Frank (Engineering)</td>
<td>88</td>
</tr>
<tr data-table-sort-group="design">
<td>Alice (Design)</td>
<td>92</td>
</tr>
<tr data-table-sort-group="sales">
<td>Bob (Sales)</td>
<td>78</td>
</tr>
</tbody>
</table>
</table-sortable>
The attribute belongs on each row, not on <tbody>, because sorting removes
the original <tbody> elements and rebuilds them. The row-level value preserves
each row's group membership while rows move. After sorting, rows with the same
value are collected into a new <tbody>.
Use a normal <tbody> around the table's data rows, as shown above. You do not
need to provide a separate <tbody> for each group. Separate <tbody> elements
are allowed in the initial markup, but they do not define the groups; the
data-table-sort-group values on the rows do.
As a shorthand, you can put data-table-sort-group on a <tbody> to give all
of its rows the same group:
<tbody data-table-sort-group="engineering">
<tr>
<td>Charlie (Engineering)</td>
<td>85</td>
</tr>
<tr>
<td>Frank (Engineering)</td>
<td>88</td>
</tr>
</tbody>
A data-table-sort-group value directly on a <tr> overrides the value
inherited from its <tbody>. The component writes the effective group value to
each generated <tbody> so the grouping is preserved across subsequent sorts.
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 |
Accessible description for an unsorted column button | "Click to sort ascending" |
label-ascending |
Accessible description and announcement when sorted ascending | "sorted ascending. Click to sort descending" |
label-descending |
Accessible description and announcement when sorted descending | "sorted descending. Click to sort ascending" |
<table-sortable
label-sortable="Cliquez pour trier par ordre croissant"
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)ariaNotify() API when availableProgressive 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