ARIA ATTRIBUTE•Widget Attributes
aria-sort
Indicates if items in a table or grid are sorted in ascending or descending order. Applied to column or row headers.
Value Type
Token
Valid Values
ascending | descending | none | other
Used With
columnheader, rowheader
Overview
The aria-sort attribute indicates the current sort direction for sortable table columns. It helps screen reader users understand which column is sorted and in what direction.
- ascending - Items sorted A to Z, smallest to largest
- descending - Items sorted Z to A, largest to smallest
- none - Column is sortable but not currently sorted
- other - Sorted by an algorithm other than ascending/descending
Live Demo: Sortable Table
| Alice Johnson | 32 | Engineering |
| Bob Smith | 28 | Marketing |
| Carol Williams | 45 | Engineering |
| David Brown | 37 | Sales |
| Eve Davis | 29 | Marketing |
Current sort: None. Click column headers to cycle through: none → ascending → descending → none.
Code Examples
Basic Usage
<!-- aria-sort on table column headers -->
<table>
<thead>
<tr>
<!-- Column with ascending sort -->
<th aria-sort="ascending">
<button>
Name
<span aria-hidden="true">▲</span>
</button>
</th>
<!-- Column with descending sort -->
<th aria-sort="descending">
<button>
Age
<span aria-hidden="true">▼</span>
</button>
</th>
<!-- Unsorted column (no sort applied) -->
<th aria-sort="none">
<button>Department</button>
</th>
<!-- Non-sortable column (no aria-sort) -->
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>32</td>
<td>Engineering</td>
<td><button>Edit</button></td>
</tr>
</tbody>
</table>All Values
<!-- aria-sort values -->
<!-- ascending: sorted lowest to highest -->
<th aria-sort="ascending" scope="col">
<button aria-label="Price, sorted ascending, click to sort descending">
Price ▲
</button>
</th>
<!-- descending: sorted highest to lowest -->
<th aria-sort="descending" scope="col">
<button aria-label="Date, sorted descending, click to remove sort">
Date ▼
</button>
</th>
<!-- none: sortable but not currently sorted -->
<th aria-sort="none" scope="col">
<button aria-label="Name, click to sort ascending">
Name
</button>
</th>
<!-- other: sorted by a custom algorithm (rare) -->
<th aria-sort="other" scope="col">
<button aria-label="Relevance, sorted by relevance">
Relevance
</button>
</th>
<!-- No aria-sort: column is not sortable -->
<th scope="col">
Actions
</th>Complete Implementation
<!-- Complete sortable table implementation -->
<table aria-label="Employee directory, sortable">
<caption>
Click column headers to sort. Currently sorted by
<span id="sort-status">Name ascending</span>
</caption>
<thead>
<tr>
<th scope="col" aria-sort="ascending">
<button
aria-describedby="sort-instructions"
onclick="sortTable('name')"
>
Name
<span aria-hidden="true">▲</span>
</button>
</th>
<th scope="col" aria-sort="none">
<button
aria-describedby="sort-instructions"
onclick="sortTable('department')"
>
Department
</button>
</th>
<th scope="col" aria-sort="none">
<button
aria-describedby="sort-instructions"
onclick="sortTable('salary')"
>
Salary
</button>
</th>
</tr>
</thead>
<tbody>
<!-- Table rows -->
</tbody>
</table>
<!-- Hidden instructions for screen readers -->
<div id="sort-instructions" class="sr-only">
Activate to sort by this column
</div>
<!-- Live region for sort announcements -->
<div aria-live="polite" class="sr-only" id="sort-announcement">
</div>React Component
// React sortable table with aria-sort
import { useState, useMemo, useCallback } from 'react';
type SortDirection = 'ascending' | 'descending' | 'none';
interface Column<T> {
key: keyof T;
label: string;
sortable?: boolean;
}
interface SortableTableProps<T extends Record<string, unknown>> {
data: T[];
columns: Column<T>[];
caption?: string;
}
function SortableTable<T extends Record<string, unknown>>({
data,
columns,
caption
}: SortableTableProps<T>) {
const [sortColumn, setSortColumn] = useState<keyof T | null>(null);
const [sortDirection, setSortDirection] = useState<SortDirection>('none');
const [announcement, setAnnouncement] = useState('');
const sortedData = useMemo(() => {
if (!sortColumn || sortDirection === 'none') return data;
return [...data].sort((a, b) => {
const aVal = a[sortColumn];
const bVal = b[sortColumn];
let comparison = 0;
if (typeof aVal === 'string' && typeof bVal === 'string') {
comparison = aVal.localeCompare(bVal);
} else if (typeof aVal === 'number' && typeof bVal === 'number') {
comparison = aVal - bVal;
}
return sortDirection === 'ascending' ? comparison : -comparison;
});
}, [data, sortColumn, sortDirection]);
const handleSort = useCallback((column: keyof T) => {
let newDirection: SortDirection;
if (sortColumn !== column) {
newDirection = 'ascending';
} else {
// Cycle: none -> ascending -> descending -> none
const cycle: SortDirection[] = ['none', 'ascending', 'descending'];
const currentIndex = cycle.indexOf(sortDirection);
newDirection = cycle[(currentIndex + 1) % cycle.length];
}
setSortColumn(column);
setSortDirection(newDirection);
// Announce to screen readers
const columnLabel = columns.find(c => c.key === column)?.label;
if (newDirection === 'none') {
setAnnouncement(`Sort removed from ${columnLabel}`);
} else {
setAnnouncement(`${columnLabel} sorted ${newDirection}`);
}
}, [sortColumn, sortDirection, columns]);
const getAriaSort = (column: keyof T): SortDirection | undefined => {
if (sortColumn !== column) return undefined;
return sortDirection;
};
const getSortIcon = (column: keyof T) => {
if (sortColumn !== column || sortDirection === 'none') {
return '↕'; // Unsorted indicator
}
return sortDirection === 'ascending' ? '▲' : '▼';
};
return (
<>
<table aria-label={caption}>
{caption && <caption className="sr-only">{caption}</caption>}
<thead>
<tr>
{columns.map(column => (
<th
key={String(column.key)}
scope="col"
aria-sort={column.sortable ? getAriaSort(column.key) : undefined}
>
{column.sortable ? (
<button
onClick={() => handleSort(column.key)}
aria-label={`${column.label}, ${
getAriaSort(column.key) === 'ascending' ? 'sorted ascending' :
getAriaSort(column.key) === 'descending' ? 'sorted descending' :
'click to sort'
}`}
>
{column.label}
<span aria-hidden="true">
{getSortIcon(column.key)}
</span>
</button>
) : (
column.label
)}
</th>
))}
</tr>
</thead>
<tbody>
{sortedData.map((row, i) => (
<tr key={i}>
{columns.map(column => (
<td key={String(column.key)}>
{String(row[column.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
{/* Live region for announcements */}
<div aria-live="polite" className="sr-only">
{announcement}
</div>
</>
);
}
// Usage
function App() {
const data = [
{ name: 'Alice', department: 'Engineering', salary: 95000 },
{ name: 'Bob', department: 'Marketing', salary: 75000 },
{ name: 'Carol', department: 'Sales', salary: 85000 },
];
const columns = [
{ key: 'name' as const, label: 'Name', sortable: true },
{ key: 'department' as const, label: 'Department', sortable: true },
{ key: 'salary' as const, label: 'Salary', sortable: true },
];
return (
<SortableTable
data={data}
columns={columns}
caption="Employee directory"
/>
);
}Best Practices
Do
- • Use on <th> with columnheader/rowheader role
- • Update aria-sort when sort changes
- • Provide visual sort indicators (▲ ▼)
- • Use aria-live region for sort announcements
- • Include scope="col" on column headers
Don't
- • Add to non-sortable columns
- • Forget to update on sort change
- • Use without visual indication
- • Add to multiple columns simultaneously
- • Use on elements other than headers

