Skip to content
easeweb
Search topics

Expandable table rows a keyboard can't open

A task table opens a task's subtasks only when the arrow in its row is clicked, so a keyboard can't open them and a screen reader isn't told the row opens.

WCAG 2.2
SC 1.3.1ASC 4.1.2ASC 2.1.1A
Required by
Section 508 · EN 301 549 (EU)
Broken <span class="toggle"></span>
The accessibility problem illustrated: Motor:How do I see its subtasks?
01

The problem

Keyboard users can't open a task in the table to reach its subtasks, and screen reader users aren't told which tasks open, whether they are open, or which rows are subtasks.

02

Try it

Open Plan the summer sale and reach Book the newsletter slot using only the keyboard.

  • Broken 1: press Tab. It stops on the task links already showing, never on an arrow, so Plan the summer sale can't be opened.

  • Broken 2: the same table, with role="treegrid" added. Tab still skips both arrows, the arrow keys do nothing, and the panel never says a row's level or whether a task is open.

  • Apply a fix. With the button, Tab reaches each arrow and Enter opens its task; in the treegrid, the arrow keys move by row and open tasks, Right moves into a row's cells, and Home and End jump to the ends.

Broken Can you fix this?

Tasks

TaskAssigneeDue
Launch the spring catalogueAna RuizMar 14
Write product copyBen CarterMar 7
Shoot product photosDana ParkMar 10
Renew the domainEli NovakMar 20
Plan the summer saleChris MoreauApr 2
Completed tasks

Screen reader says:

(Tab into the table)

A recreated demo. The broken version is intentionally inaccessible.

03

The fix

Let the keyboard open each row, and say whether it is open.

Before

An arrow that only takes clicks

<h2 id="tasks-title">Tasks</h2><table class="tasks" aria-labelledby="tasks-title">  <thead>    <tr>      <th scope="col">Task</th>      <th scope="col">Assignee</th>      <th scope="col">Due</th>    </tr>  </thead>  <tbody>    <tr id="task-101" class="is-open">      <td>        <span class="toggle"></span>        <a href="/tasks/101">Launch the spring catalogue</a>      </td>      <td>Ana Ruiz</td>      <td>Mar 14</td>    </tr>    <tr class="subtask" data-parent="task-101">      <td>        <a href="/tasks/102">Write product copy</a>      </td>      <td>Ben Carter</td>      <td>Mar 7</td>    </tr>    <tr class="subtask" data-parent="task-101">      <td>        <a href="/tasks/103">Shoot product photos</a>      </td>      <td>Dana Park</td>      <td>Mar 10</td>    </tr>    <tr>      <td>        <a href="/tasks/104">Renew the domain</a>      </td>      <td>Eli Novak</td>      <td>Mar 20</td>    </tr>    <tr id="task-105">      <td>        <span class="toggle"></span>        <a href="/tasks/105">Plan the summer sale</a>      </td>      <td>Chris Moreau</td>      <td>Apr 2</td>    </tr>    <tr class="subtask" data-parent="task-105" hidden>      <td>        <a href="/tasks/106">Choose the discounts</a>      </td>      <td>Ana Ruiz</td>      <td>Mar 25</td>    </tr>    <tr class="subtask" data-parent="task-105" hidden>      <td>        <a href="/tasks/107">Book the newsletter slot</a>      </td>      <td>Ben Carter</td>      <td>Mar 28</td>    </tr>  </tbody></table><a href="/tasks/completed">Completed tasks</a>
What is wrong3 points
  • The arrow is a span that takes no focus and responds only to click, so a closed task's subtasks are out of reach for the keyboard.
  • Nothing in the markup says a task opens, or whether it is open.
  • Nothing says the rows after a task are its subtasks: the nesting is padding.
After

A button in the row, and subtasks that say so

<h2 id="tasks-title">Tasks</h2><table class="tasks" aria-labelledby="tasks-title">  <thead>    <tr>      <th scope="col">Task</th>      <th scope="col">Assignee</th>      <th scope="col">Due</th>    </tr>  </thead>  <tbody>    <tr id="task-101">      <td>        <button type="button" class="toggle" aria-label="Subtasks" aria-expanded="true"></button>        <a href="/tasks/101">Launch the spring catalogue</a>      </td>      <td>Ana Ruiz</td>      <td>Mar 14</td>    </tr>    <tr class="subtask" data-parent="task-101">      <td>        <a href="/tasks/102">Write product copy<span class="visually-hidden">, subtask</span></a>      </td>      <td>Ben Carter</td>      <td>Mar 7</td>    </tr>    <tr class="subtask" data-parent="task-101">      <td>        <a href="/tasks/103">Shoot product photos<span class="visually-hidden">, subtask</span></a>      </td>      <td>Dana Park</td>      <td>Mar 10</td>    </tr>    <tr>      <td>        <a href="/tasks/104">Renew the domain</a>      </td>      <td>Eli Novak</td>      <td>Mar 20</td>    </tr>    <tr id="task-105">      <td>        <button type="button" class="toggle" aria-label="Subtasks" aria-expanded="false"></button>        <a href="/tasks/105">Plan the summer sale</a>      </td>      <td>Chris Moreau</td>      <td>Apr 2</td>    </tr>    <tr class="subtask" data-parent="task-105" hidden>      <td>        <a href="/tasks/106">Choose the discounts<span class="visually-hidden">, subtask</span></a>      </td>      <td>Ana Ruiz</td>      <td>Mar 25</td>    </tr>    <tr class="subtask" data-parent="task-105" hidden>      <td>        <a href="/tasks/107">Book the newsletter slot<span class="visually-hidden">, subtask</span></a>      </td>      <td>Ben Carter</td>      <td>Mar 28</td>    </tr>  </tbody></table><a href="/tasks/completed">Completed tasks</a>
How it is fixed5 steps
  1. Make the arrow a native button named Subtasks. Tab reaches it, and Enter and Space open the task with the click the script already listens for.
  2. Give it aria-expanded, and set it in the click handler. A screen reader says "Subtasks, button, collapsed" or "expanded" just before the task's own link.
  3. End each subtask's link with hidden words, so it is read as "Write product copy, subtask", whether it is reached by Tab or by moving down the Task column. Its name still starts with the words on screen, which voice control users say.
  4. Keep hidden on a closed task's rows, so they stay out of the Tab order. The open task's rows follow its button, so the next Tabs go through them.
  5. Keep the table's headers, and draw the arrow from aria-expanded rather than a class.

This fixes both Befores and passes 1.3.1, 2.1.1 and 4.1.2. Several table libraries already draw the arrow this way: Element Plus, for one, renders it as a button with aria-expanded.

Why this fix: Use native HTML first

04

Design ideas

A row can open quietly and still open from a key.

Five ways to draw a task that opens into its subtasks. Open each with the mouse, then put the mouse aside and Tab through them: three open from the keyboard, and two only with a mouse.

In the design file

  • Draw every row that opens twice, closed and open, with its arrow turned in each.
  • Draw the focus ring on the arrow, and keep the arrow shown without hovering.
  • Write the arrow’s name beside it, such as “Subtasks”, since it becomes the name a screen reader reads.
  • Write how a row under a task says it is one, in words, not only by indenting it.

Passes

Fails

05

Verify the fix

5 checks, no mouse.

Test with: Keyboard, Screen reader, Forced colors, Zoom

0 of 5 checked

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the table, its component or its library changes.

How screen readers handle aria-expanded

Limits

This is a starting pattern, not a guarantee for every application. The scripts are minimal sketches: the treegrid has no Page Up or Page Down, no selection, no type-ahead and no loading of subtasks on demand. Tab always leaves the grid, where the pattern moves it on to the next link in the same row first. A sidebar of sections and pages with no columns is a tree view, covered in tree views keyboards can't open and screen readers flatten, and a table whose cells are edited in place is covered in editable data grids a keyboard can't move through or edit. A single section that opens and closes is covered in show and hide buttons that don't say whether they are open. Test the complete journey with your target assistive technology.

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

EaseWeb service

Fixed it? Keep it from coming back.

I turn guides like this one into rules your coding agents follow and checks your CI runs. Then I retest by hand with a keyboard and screen readers, periodically.

See the service