Skip to content
easeweb
Search topics

Sortable tables that show the sort only as an arrow

Pressing a column header re-sorts the rows and moves an arrow, but no header says it sorts, or which way, so a screen reader hears plain headers.

WCAG 2.2
SC 4.1.2A
Required by
Section 508 · EN 301 549 (EU)
Broken <svg class="arrow" aria-hidden="true">
The accessibility problem illustrated: No vision:Which way is it sorted?
01

The problem

Screen reader users can't tell which column a table is sorted by, or which way, and may not learn that its headers sort at all.

02

Try it

Sort the orders by Total, then find out from the panel which way they run.

  • Broken 1: click Total. The rows re-sort and the arrow moves. Then try Tab: it never reaches the headers. The panel shows what a screen reader says on the header you clicked.

  • Broken 2: Tab to Date and Total, and press Enter on Total. The panel shows what a screen reader says. Does it tell you the order?

  • Apply the fix. Tab to Date, then press Enter on Total, twice. Does the panel say which way the rows run?

Broken Can you fix this?

Your orders
OrderDateTotalStatus
#107122 Oct 2026$9.90Processing
#106318 Oct 2026$142.00Shipped
#105712 Oct 2026$24.50Delivered
#10423 Oct 2026$86.00Delivered

Screen reader says:

(Click a sortable header)

A recreated demo. The broken version is intentionally inaccessible.

03

The fix

Make each header a button, and say the sort with aria-sort.

Before

The header cell takes the click

<table class="orders">  <caption>Your orders</caption>  <thead>    <tr>      <th scope="col">Order</th>      <th scope="col" class="sorted-desc" data-key="date">        Date        <svg class="arrow" aria-hidden="true">…</svg>      </th>      <th scope="col" data-key="total">        Total        <svg class="arrow" aria-hidden="true">…</svg>      </th>      <th scope="col">Status</th>    </tr>  </thead>  <tbody>    <tr>      <td>#1071</td>      <td>22 Oct 2026</td>      <td>$9.90</td>      <td>Processing</td>    </tr>    …  </tbody></table>
What is wrong3 points
  • The Date and Total headers take the click themselves, so they are read as column headers, not buttons, and nothing says they can be pressed. Tab never reaches them either, a failure covered in div buttons that can't be reached with Tab.
  • The arrow is an icon hidden from screen readers, shown and turned by classes the browser knows nothing about, so the sort is shown only to the eye.
  • No header has aria-sort, so neither the sorted column nor its direction is ever said.
After
FixA button in each header, and aria-sort on the sorted one
<table class="orders">  <caption>    Your orders    <span class="visually-hidden">, column headers with buttons are sortable</span>  </caption>  <thead>    <tr>      <th scope="col">Order</th>      <th scope="col" aria-sort="descending">        <button type="button" data-key="date">          Date          <svg class="arrow" aria-hidden="true">…</svg>        </button>      </th>      <th scope="col">        <button type="button" data-key="total">          Total          <svg class="arrow" aria-hidden="true">…</svg>        </button>      </th>      <th scope="col">Status</th>    </tr>  </thead>  <tbody>    <tr>      <td>#1071</td>      <td>22 Oct 2026</td>      <td>$9.90</td>      <td>Processing</td>    </tr>    …  </tbody></table>
How it is fixed4 steps
  1. Put each sortable header's text in a button, named by the column alone. Tab reaches it, Enter and Space press it, and it is read as "Date, button".
  2. Set aria-sort="descending" on the Date header as the page loads, and move it on every press: take it off every header, then set it, ascending or descending, on the one pressed. A screen reader now says that the column is sorted, and which way, with the header.
  3. Draw the arrow from aria-sort instead of a class, so what is drawn and what is read can't disagree, and keep aria-hidden on the icon so it adds nothing to the button's name.
  4. Add a visually hidden note to the caption, ", column headers with buttons are sortable", so the table says once that its headers sort, without adding words to every header.

This fixes both Befores and passes 4.1.2. Order and Status have no button, so they take no aria-sort either.

04

Design ideas

A sort arrow has more than one look.

Five more ways to draw the headers of the same orders table. Press them and Tab to them: four say the order by shape or in words, and one only by colour.

In the design file

  • Draw each sortable header at rest, sorted each way, on hover and on focus.
  • Show the direction by the arrow’s shape, pointing the way the rows run, never by colour alone.
  • Write which columns sort, and which one the table opens sorted by, and which way.
  • Keep each header’s words as the column’s name, and draw a line such as “Newest first” for the eye only.

Passes

  • Order
    #107122 Oct$9.90
    #106318 Oct$142.00
    Up-down arrow until sorted Every column that sorts carries a grey up-down arrow, 4.83:1 on white, so people can see which ones sort before they press. The sorted one swaps it for a single arrow pointing the way the rows run.
  • Order
    #107122 Oct$9.90
    #106318 Oct$142.00
    Sorted header in a soft pill A tinted pill marks the sorted column, as a chosen filter chip does, with the arrow inside it for the direction. The #1d4ed8 text keeps 5.80:1 on the #e8effd tint.
  • Order
    #107122 Oct$9.90
    #106318 Oct$142.00
    The order in words A small grey line under the sorted header, “Newest first”, says what an arrow leaves to a guess: does down mean newest or oldest? Write the words for each column and both directions.
  • OrderDateTotal
    #107122 Oct$9.90
    #106318 Oct$142.00
    A Sort by menu above the table Shops often put the order in a menu above the list instead. “Sort by: Newest first” names the order in words and works alike with a mouse, a keyboard and a phone. The headers stay plain, so nothing suggests that they sort.

    Rule of thumb: Use native HTML first

Fails

  • Order
    #107122 Oct$9.90
    #106318 Oct$142.00
    Two carets, the active one in colourWCAG 1.4.1 Both carets are always there, and the one that gives the direction turns #2563eb blue beside a #6b7280 grey: 1.06:1 between them, so only the hue tells them apart, and someone who can’t see the blue can’t tell up from down. Hide the other caret on the sorted column, or draw a single arrow.
05

Verify the fix

5 checks, no mouse.

Test with: Screen reader, Keyboard

0 of 5 checked

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the table component changes.

Limits

This is a starting pattern, not a guarantee for every table. renderRows stands in for your own code that redraws the body. Screen readers word aria-sort differently, and one may read it only when focus enters the header cell, not on every press. When sorting also filters the rows, or a search narrows them, the number of rows shown is a separate failure, covered in search and filter results that update without a word.

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

EaseWeb service

Fixed it? Keep it from coming back.

I turn guides like this one into rules your coding agents follow and checks your CI runs. Then I retest by hand with a keyboard and screen readers, periodically.

See the service