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,noneorother, 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.
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.
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 |
Step-by-step implementation
Permalink to "Step-by-step implementation"- Move state to the header. Find every place your code sets sort state and make it write
aria-sorton theth. Remove state words from button labels. - Hide the icon. Add
aria-hidden="true"andfocusable="false"to inline SVGs, or use CSS background images. Icon fonts needaria-hiddenon their element as well. - 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.
- Check contrast and forced colours. The solid arrow must meet 3:1 against the header background, and
currentColoror system colours keep it visible in forced colours mode. - 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-sortuntil 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-sortonly 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.
Related
Permalink to "Related"- Multi-column sort announcement patterns — announcing sorts
- aria-sort attributes — the attribute in depth
- Column header menus — sorting from menus