Sortable Tables with Progressive Enhancement
Many years ago, I made a little jQuery plugin called Easy Sortable Tables. It works exactly as promised: point it at a table and it upgrades the UI so a reader can sort the rows by clicking a column heading. The functionality is still useful, but I can’t even remember the last time I added jQuery to a site.
The web platform’s changed quite a bit since I wrote that plugin, so I decided to modernize it as a web component: <table-sortable>.
It all starts with a table
Just like the original, this web component upgrades a real table when you wrap it:
<table-sortable>
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
<tbody>
<tr>
<td>Charlie Brown</td>
<td>35</td>
<td>New York</td>
</tr>
<tr>
<td>Alice Cooper</td>
<td>28</td>
<td>Boston</td>
</tr>
<tr>
<td>Bob Dylan</td>
<td>42</td>
<td>Chicago</td>
</tr>
</tbody>
</table>
</table-sortable>
If the custom element never loads, the browser rightfully ignores it—thanks to the fault-tolerant nature of HTML—and renders the <table>. People can read it, navigate it, copy from it, or print it in the order it’s written.
When the element initializes, it turns each table header’s text into a button. I chose actual <button> elements over <div>s because they naturally participate in the tab order and respond to Enter and Space. A click on “Age” sorts the rows by age; the next click reverses them.
That interaction changes more than the row order. The script updates aria-sort on the active <th> and associates the button with a description of its current state and next action using aria-describedby. The button’s name remains the column heading.
What you see isn’t always what to sort
If your table contains just strings and numbers, that’s all you really need. If, however, it contains things like people’s names, prices, and dates, you’re probably going to want to customize the way the data sorts.
You can customize the way a value sorts by using data-sort-value:
<tr>
<td data-sort-value="WIDGET-B">Widget B (Premium)</td>
<td data-sort-value="50">$50.00</td>
<td data-sort-value="2026-10-05">October 5, 2026</td>
</tr>
Readers see a human-friendly value; the sorting logic sees a computer-friendly one.
If you want to sort by only a portion of the value, this component supports that too. For example, if you want to sort a name like “John Smith” by “Smith,” you can indicate that with data-sort-as:
<td>John <span data-sort-as>Smith</span></td>
With this in place, the component puts “Smith” before the rest of the cell’s value when it builds the sort key. People can still read the name naturally, but the table sorts it by last name.
Stay with your group!
If you’re working with a particularly complex table, you might want to keep related rows together based on a shared characteristic, such as people within the same department. You can do that with data-table-sort-group. Put it on a <tbody> to give all of its rows the same group:
<tbody data-table-sort-group="engineering">
<!-- Engineering rows -->
</tbody>
You can also apply data-table-sort-group to individual <tr> elements. Rows with the same value will stay together, regardless of whether they started in the same <tbody>. If you happen to use both approaches, the value on the <tr> takes precedence.
Accessible by default
Accessibility isn’t a nice-to-have; it’s a core principle of all my work. And so it is with this web component. Here are a few of its accessibility features:
- True
<button>elements that receive focus and work with any input method - UI state represented with ARIA
- The real
<table>remains in the light DOM, preserving its core structure and semantics - Sort state exposed using
aria-sort, with button descriptions associated usingaria-describedby
I also took care to make those descriptions adaptable to whatever language your audience speaks.
¿En qué dirección vas?
Since I primarily speak English, the sort descriptions default to English. You can adapt them to another language—or just override my phrasing—with a few attributes. Here’s an example en français:
<table-sortable
label-sortable="Cliquer pour trier par ordre croissant"
label-ascending="trié croissant. Cliquer pour trier décroissant"
label-descending="trié décroissant. Cliquer pour trier croissant"
>
<!-- the table -->
</table-sortable>
Sort with style
Because <table-sortable> works in the light DOM, it gives designers a remarkable amount of control over the table’s styles. Here are a few useful selectors:
th[aria-sort="none"]- column header in the unsorted stateth[aria-sort="ascending"]- column header in the ascending sort stateth[aria-sort="descending"]- column header in the descending sort stateth[aria-sort] > button- the button used to handle sorting; combine with above to generate directional arrows into the UIcol.sorted- the currently sorted column
You’ll see these put to good use in the demo below.
Get yourself sorted
You can try the examples in the live demo.
If you’d like to put this web component to use on your own site, you can find out more on the <table-sortable> project page.
If you use it on your site, please do let me know. The incredibly talented Zach Leatherman—creator of Eleventy and an impressive number of useful web components—did, and he called it “a very good web component!”, which was awfully nice to hear.
Update 2026-10-17
Adrian Roselli took a look at the component and offered some excellent feedback on how different screen readers announce the sorting updates and better visual affordances for sighted users. I’ve made a few minor changes to the component and demo in an effort to address it:
- I removed the
ariaNotify()calls and live-region fallback. The component now relies onaria-sortand the button descriptions rather than adding an announcement that can duplicate what a screen reader already says. - I updated the demo to include an example with SVG sort indicators.
I updated the relevant sections above too. These changes don’t make every browser and screen reader behave alike; as Adrian points out, it’s worth testing with the combinations your audience uses. Thanks, Adrian!
Webmentions
@Aaron I finally got a chance to look at this. I have some feedback; is this best channel for it?
@aardrian Thank you! Whatever’s easiest for you. GitHub Issues are how I’ll ultimately track, but I can translate your feedback to them if you like.
https://github.com/aarongustafson/table-sortable/issues
Issues · aarongustafson/table-sortableMade a few accessibility upgrades, prompted by some excellent feedback from @aardrian.
https://www.aaron-gustafson.com/notebook/sortable-tables-with-progressive-enhancement/#update-2026-10-17
Sortable Tables with Progressive EnhancementLikes
Shares