Sortable tables (v2): don’t hide sorting state
Sortable tables are still tables. The “sortable” part is a control and a state change.
If a user can’t sort with keyboard and can’t perceive the current sort order without vision, it’s not done yet.
Checklist (small, reliable pattern)
- Keep real table semantics. Use
<table>, <caption>, <thead>, <tbody>, and <th scope="col">.
- Put a real control in the header. Example:
<th><button>Score</button></th>. Don’t style plain text to “look clickable”.
- Expose state with
aria-sort. Put aria-sort="ascending" / "descending" on the active column header cell (<th>).
- Keep focus stable. Sorting shouldn’t kick keyboard users back to the top. Prefer updating only the table body, not rebuilding headers.
- Announce the change (politely). A short
role="status" line like “Sorted by Score (descending).” helps screen readers and sighted users alike.
- Handle ties predictably. Use a stable tie-breaker so equal values don’t reshuffle unexpectedly.
- Support clearing sort state. Consider cycling: ascending → descending → none (or a Clear sort button).
Common pitfalls
- Clicking the whole
<th> is not a substitute for a button. It’s harder to focus, harder to name, and easy to implement incorrectly.
- Visual arrows alone aren’t enough. Keep them, but treat them as decoration (
aria-hidden) and expose the true state with aria-sort.
- String-sorting numbers. “1205” shouldn’t come before “980”. Parse numeric columns as numbers.
Rule of thumb
Tab to a column header, press Space, hear/see “Sorted by …”, and stay in the header. That’s the experience to aim for.