Loading Developer Playground

Loading ...

Skip to main content
ARIA ATTRIBUTEWidget 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 Johnson32Engineering
Bob Smith28Marketing
Carol Williams45Engineering
David Brown37Sales
Eve Davis29Marketing

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

Related Attributes

Specifications & Resources