Multi-Column Sortable Table Experiment

This post expands on what I covered in my April 2021 post, Sortable Table Columns. You may want to read that first to understand the broader challenges and techniques for making a table sortable by one column at a time.

That last statement is what matters here.

ARIA 1.1 says authors SHOULD apply aria-sort to only one header at a time. We have seen plenty of use cases for more than one column to allow sorting at a time, and ARIA does not forbid it. This post attempts to show how you can approach it.

This post does not, however, tell you exactly what to do as that is dependent on your users.


In this example you can sort all the columns. When sorted, each column header gets a number showing its priority for sorting data. If two columns are sorted, the column with priority 1 sorts the data first. The column with priority 2 sorts within that. And so on.

The first time you choose a column header, it sorts descending and makes it the first priority. The second time you choose that column header, it sorts it ascending and makes it the first priority (in case it was not already). The third time you choose that column header it removes sorting for that column and sets the column that was second in priority to be first (adjusting others down the line).

As in my other post, the data in the table does not get sorted. It is only there for show. Unlike in my other post, I show the text that appears in the live region so you can see what information I expose to screen readers.

For VoiceOver on macOS, you will need to restart VoiceOver in order for it to recognize when you change the live region between off, polite, and assertive.

The example is embedded below, or you can edit it on Codepen, or test it with your own AT in debug mode on Codepen.

See the Pen Multi-Column Sortable Table by Adrian Roselli (@aardrian) on CodePen.

This chunk of HTML shows two column headers, one sorted and one not:

<th id="ColAuthor" aria-sort="descending" data-priority="5" aria-describedby="SortPriText SortPriorityColAuthor">
 <button type="button" id="ColAuthorSortButton" onclick="[…]">
  <svg […]>[…]</svg>
  <svg […]>[…]</svg>
 <span class="sortpriority" id="SortPriorityColAuthor" aria-hidden="true">5</span>
<th id="ColTitle">
 <button type="button" id="ColTitleSortButton" onclick="[…]">
  <svg […]>[…]</svg>
  <svg […]>[…]</svg>
 <span class="sortpriority" id="SortPriorityColTitle" aria-hidden="true"></span>

Visually Conveying Priority

Visually I use the following to show the sort priority:

<span class="sortpriority" id="SortPriorityColAuthor" aria-hidden="true">5</span>

The aria-hidden="true" hides it from screen readers so it does not become part of the accessible name (otherwise users might hear “Title 5” or “Author 1” as they navigate the table).

I will spare you the styles I use to visually style it because I think the following is more important:

th .sortpriority:empty {
  display: none;

The :empty CSS visually hides it when it has no value.

When the number is visible, it has a transparent border so the circle will still appear when the user is in Windows High Contrast Mode:

th .sortpriority {
  border: .05em solid transparent;

Visually it may not be compelling, and you are welcome to experiment with other design choices, but the aria-hidden should be retained.

Conveying Priority on Sort

Remember in my Sortable Table Columns post I walk through the variety of ways screen readers handle notifying the user that they have triggered a sort. It ranges from helpful feedback to silence.

When sorting additional columns past the first, the screen reader will not know what else is sorted so it cannot convey this to a user. In this case you will probably need to use a live region, no matter how your target screen readers announce a sort action. That can be verbose, however.

In the example, I construct a message for screen reader users that walks through every sorted cell and conveys that to the user. I duplicate the message at the top so you can see it (the real live region gets cleared after it gets announced). Following is an example message when sorting a column when others are already sorted:

sorted up, Author descending priority 2, ISBN-10 descending priority 3

In that example, a user has just sorted the Year column, which the user will know already from navigating to it, and Author and ISBN-10 were already sorted. The columns are not read in order of sort priority, but that is just how I wrote my script. You may find from users that you do not need to say how the other columns are sorted (ascending or descending), which can reduce verbosity.

Worth noting: when JAWS 2020 unsorts a column, it announces the action as “sorted”. Which means when a user unsorts the Year column, they will hear “sorted, sorting removed…” with the rest of the live region continuing from there.

Conveying Priority on Browse

We want to be sure we do not make the sort priority part of the column header’s accessible name. For example, you might see the data-priority in my sample code and think you can just use content: attr(data-priority) with a th::after selector. If you do that, as screen reader users navigate the cells they will hear that number as part of the column header every time. It will be confusing.

What I do is dynamically add aria-describedby, pointing to both a hidden node with the word “priority” and the cell’s own <span class="sortpriority"> we saw before. When the column is unsorted I remove the aria-describedby.

There are significant caveats to this, however. Just as you cannot rely on dynamic accessible names, you run into similar risks with dynamic accessible descriptions.

In NVDA, while you can see the value in the accessible tree it is not announced. In JAWS, it is only announced when navigating through the buttons, not by column headers — even though the attribute is on the <th>. VoiceOver on iOS ignores the aria-describedby, but VoiceOver on macOS announces it before it announces the cell name.

The Script

…is crap. Do not use it. If you wade into it you will at least see how I attempt to lean on existing nodes and attributes. You will also see how I manipulate the values on the data-priority attribute because there is no native or ARIA way of tracking that. I am sure the latter part is buggy, but it was warm today and I was a little dehydrated.


Unlike my Sortable Table Columns post, this has not been run through with users or a full suite of testing tools. As with that post, this contains localization risks and plenty of ways to add or remove information based on your audience. This also fails to address aria-sort="other"

I do not consider this ready for use, but I do think it demonstrates some of the considerations and challenges you and your users will have as they try to understand this kind of pattern.

If you have feedback, thoughts, corrections then please offer them in the comments or on the Twitters.

Update: 29 June 2022

I should have linked #283 aria-sort should be allowed on multiple columns, which discusses how to identify how the columns in a set of sorted columns are ranked. ARIA does not forbid having multiple columns sorted at once, but it provides no programmatic method to convey their relative rank.

I added this post as an example, partly because I cannot show client tables that do this (both correctly and not, including some I have built). If you have examples, consider adding them so that some time in the future perhaps we won’t need verbose accessible names/descriptions that may not translate.



I have a curiosity regarding the sort of columns when there is more than one concatenated value in the cell.

In response to Gilson Jose dos Santos. Reply

I don’t see a question here, but if there is more than one value that warrants sorting in a cell then those should maybe be separate cells. I don’t know how concatenation factors into it.

Leave a Comment or Response

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>