Sort Direction Icons and Their Accessible Names

Permalink to "Sort Direction Icons and Their Accessible Names"

Sort arrows are tiny, but they carry three states and an action. Common failures: the icon is an SVG with no hiding, so screen readers announce “image” or the path data; the button is named “Sort ascending” even though it is already ascending; the active state is shown only by colour; and the icon disappears in Windows High Contrast. The fix is a clear division of labour — aria-sort reports the state, the button name reports the column, and the icon is decoration that works in every colour mode.

This guide belongs to sortable and filterable data grids.

Spec reference

Permalink to "Spec reference"
  • aria-sort — ascending, descending, none or other, on the column header cell; only one column should have a non-none value at a time (multi-sort conveys secondary sorts in text).
  • SC 1.1.1 Non-text Content (A) — decorative icons hidden with aria-hidden="true".
  • SC 1.4.1 Use of Color (A) — active sort shown by more than colour.
  • SC 1.4.11 Non-text Contrast (AA) — sort icons meet 3:1.
  • SC 4.1.2 (A) — the button has a name; the header exposes state.
Who says what about sort Flow: the th element carries aria-sort for state; the button inside carries the column name for the action; the icon is aria-hidden decoration; the screen reader combines them into "Amount, button, sorted ascending". Who says what about sortth aria-sortstate: ascendingbuttonname: Amountsvg aria-hiddendecoration onlyScreen reader"Amount, sorted ascending, button"
State on the header, name on the button, icon silent.

When to use — and when not to

Permalink to "When to use — and when not to"

Use this pattern for every sortable column header. If sorting happens elsewhere (a sort dropdown above the table), still set aria-sort on the sorted header so the state is discoverable during table navigation.

Do not put state words in the button name (“Amount, sorted ascending, click to sort descending”); the screen reader already announces aria-sort, and the duplicate is verbose and drifts out of date. The misapplication: changing the button’s aria-label on each click and leaving aria-sort unset.

State in the name versus state in aria-sort Comparison: putting state in the button label duplicates or contradicts announcements; aria-sort on the header lets screen readers report state consistently. State in the name versus state in aria-sort✗ State in aria-label"Sort ascending" when already ascendingVerbose and inconsistentNot available in table navigationEasy to forget to update✓ State in aria-sort"Amount, sorted ascending"Concise, standardAnnounced with headerSingle source of truth

Annotated code example

Permalink to "Annotated code example"
<th scope="col" aria-sort="ascending">               <!-- state lives here -->
  <button type="button" class="sort-btn">
    Amount
    <svg class="sort-icon" aria-hidden="true" focusable="false" viewBox="0 0 12 16">   <!-- decoration -->
      <path class="up" d="M6 2l4 5H2z"/><path class="down" d="M6 14l4-5H2z"/>
    </svg>
  </button>
</th>
.sort-icon path { fill: currentColor; opacity: .3; }                 /* unsorted: both faint */
th[aria-sort="ascending"]  .sort-icon .up   { opacity: 1; }          /* shape change, not colour */
th[aria-sort="descending"] .sort-icon .down { opacity: 1; }
th[aria-sort="ascending"]  .sort-icon .down,
th[aria-sort="descending"] .sort-icon .up   { display: none; }       /* one arrow when sorted */
@media (forced-colors: active) { .sort-icon path { fill: ButtonText; } }

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
State NVDA VoiceOver
Unsorted (aria-sort absent or none) “Amount, button” “Amount, button”
Ascending “Amount, button, sorted ascending” “Amount, ascending, button”
Descending “Amount, button, sorted descending” “Amount, descending, button”
After activation State re-announced on focus; live region announces the change Same
Header row with three sort states Mock: a table header row where Invoice is unsorted with faint double arrows, Amount is focused and sorted ascending with a single solid up arrow, and Due date is unsorted. Header row with three sort statesInvoice ⇅Amount ▲Due date ⇅ @ focused, aria-sort…INV-10421,240.0014 MarINV-10432,980.002 Apr
One solid arrow marks the sorted column.

Step-by-step implementation

Permalink to "Step-by-step implementation"
  1. Move state to the header. Find every place your code sets sort state and make it write aria-sort on the th. Remove state words from button labels.
  2. Hide the icon. Add aria-hidden="true" and focusable="false" to inline SVGs, or use CSS background images. Icon fonts need aria-hidden on their element as well.
  3. Make states distinguishable without colour. Unsorted shows two faint arrows; sorted shows one solid arrow pointing in the direction. Shape and count carry the meaning even in greyscale.
  4. Check contrast and forced colours. The solid arrow must meet 3:1 against the header background, and currentColor or system colours keep it visible in forced colours mode.
  5. Announce the change. After activation, a polite status message such as “Sorted by amount, ascending” confirms the action, since some screen readers do not re-announce aria-sort until focus moves.

Integration context

Permalink to "Integration context"

Sort announcements are covered in multi-column sort announcement patterns and the attribute in aria-sort attributes for accessible column filtering. Icon colours in dark mode and forced colours are tested in visual regression and theme testing. Header menus that also sort are in column header menus in data grids.

Gotchas

Permalink to "Gotchas"

aria-sort on the button. It is only valid on columnheader and rowheader; on a button it is ignored.

Multiple aria-sort values. Only the primary sort column should carry a non-none value; secondary sorts are described in text.

Icons that flip on hover. Showing the “next” direction on hover confuses sighted users about the current state; if you preview, use a different style.

Edge cases

Permalink to "Edge cases"
  • Server-side sorting. Set aria-sort only after the sorted data arrives, or users hear “sorted ascending” over unsorted rows.
  • Custom sort orders. Use aria-sort="other" for orders such as “priority” that are neither ascending nor descending, and describe them in text.
  • Icons without buttons. If only the icon is clickable, the target is too small and unnamed; make the whole header a button.

Worked example

Permalink to "Worked example"

A grid library rendered sort icons as inline SVGs without aria-hidden and set button labels like “Sort descending”. NVDA users heard “graphic, Sort descending, button” on a column that was already descending, and the active column was shown only by blue colour. The team moved state to aria-sort, hid the icons, named buttons with the column only, used one solid versus two faint arrows, and added forced-colours rules. Announcements became “Amount, button, sorted descending” and the icons survived High Contrast mode.

Common mistakes

Permalink to "Common mistakes"
  • Unhidden SVG icons.
  • State in the button label.
  • Colour-only active state.
  • aria-sort on the wrong element.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Ship a sort header component that owns aria-sort, the icon and the status message, so product teams cannot reintroduce state-in-label patterns.

Testing it with a screen reader

Permalink to "Testing it with a screen reader"

In NVDA, tab across the header row and listen to each sort button: unsorted columns should read only the column name and “button”, and the sorted column should add its direction. Activate a button and confirm the status message, then shift-tab away and back to hear the updated state. Use the elements list (Insert+F7) to check that no “graphic” entries come from sort icons. In VoiceOver, repeat with VO+Right through the header row. Then turn on Windows High Contrast mode and confirm that the icons remain visible, and view the page in greyscale to check that the sorted column is still obvious.

FAQ

Permalink to "FAQ"
Should unsorted columns have aria-sort="none"?

Either omit it or use none; both mean unsorted. Some teams omit it to reduce verbosity.

Can the icon have alt text instead?

It would duplicate aria-sort; hide it.

How do I show the next direction?

Use a tooltip or visible hint if needed, not the button name.

Do icon fonts need special care?

Yes. Hide them with aria-hidden and check they render in forced colours.

What about multi-column sort?

Primary sort uses aria-sort; secondary sorts appear in visible text and announcements.

Is 3:1 required for the faint unsorted arrows?

The meaningful state indicator must meet 3:1; faint arrows indicating “sortable” should ideally meet it too.

Permalink to "Related"

← Back to Sortable & Filterable Data Grids