Lists and tables Required
Make sortable columns buttons
Put a button inside each sortable column header, and set aria-sort on the header of the sorted column only.
How to do it
Start from table headers. The button's name is the column name. Set aria-sort="ascending" or "descending" on the sorted th and remove it from the others. Show the direction with an icon as well as the attribute. Announce the new order in a status region, such as "Sorted by amount, highest first".
Example
<th scope="col" aria-sort="descending">
<button type="button" data-sort="amount">Amount <svg aria-hidden="true" class="sort-icon">…</svg></button>
</th>
<th scope="col"><button type="button" data-sort="customer">Customer</button></th>
export function markSorted(table: HTMLTableElement, column: string, direction: 'ascending' | 'descending') {
for (const header of table.querySelectorAll('th')) header.removeAttribute('aria-sort');
table.querySelector(`button[data-sort="${column}"]`)?.closest('th')?.setAttribute('aria-sort', direction);
}
from playwright.sync_api import expect
def test_sort_by_keyboard(page):
page.goto("https://staging.example.com/orders")
page.get_by_role("button", name="Amount").focus()
page.keyboard.press("Enter")
expect(page.locator("th[aria-sort]")).to_have_count(1)
expect(page.get_by_role("columnheader", name="Amount")).to_have_attribute("aria-sort", "ascending")
How to check
A browser test tabs to a header button and presses Enter. That header has aria-sort, no other header has it, and the status region names the order.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers app interface playbook: https://agentwranglers.se/playbook/app-ui/lists-and-tables/sortable-headers.