Keyboard Access to Hover-Revealed Row Actions
Permalink to "Keyboard Access to Hover-Revealed Row Actions"Many tables hide row actions — edit, duplicate, delete — until the pointer hovers over a row. It keeps the table clean for mouse users. For keyboard users, the actions often never appear, because they are revealed with :hover only. For screen reader users, they may be missing from the DOM until a mouseenter event fires. And on touch screens there is no hover at all. Hover-revealed actions can be accessible, but only if they are revealed by focus too and exist in the DOM all the time.
This guide belongs to context menus and row actions.
Spec reference
Permalink to "Spec reference"- SC 2.1.1 Keyboard (A) — actions operable from the keyboard.
- SC 2.4.7 Focus Visible (AA) — a focused action must be visible, not at opacity 0.
- SC 1.4.13 Content on Hover or Focus (AA) — revealed content dismissible, hoverable and persistent.
- SC 2.5.8 Target Size (AA) — revealed actions still meet size requirements.
When to use — and when not to
Permalink to "When to use — and when not to"Use hover reveal when tables are dense and actions are secondary, and only with focus reveal and a touch fallback. For primary actions, keep them always visible.
Do not render actions only on mouseenter; they will not exist for keyboard and screen reader users. The misapplication: opacity: 0 on actions with no :focus-within rule, so keyboard users tab onto invisible buttons and see nothing.
Annotated code example
Permalink to "Annotated code example".row-actions { opacity: 0; transition: opacity .15s; }
tr:hover .row-actions,
tr:focus-within .row-actions { opacity: 1; } /* keyboard reveal (SC 2.4.7) */
@media (hover: none), (pointer: coarse) {
.row-actions { opacity: 1; } /* touch: always visible */
}
@media (prefers-reduced-motion: reduce) { .row-actions { transition: none; } }
<tr>
<th scope="row">INV-1042</th><td>1,240.00</td>
<td class="actions-cell">
<div class="row-actions">
<button type="button" aria-label="Edit INV-1042">✎</button>
<button type="button" aria-label="Duplicate INV-1042">⧉</button>
<button type="button" aria-label="Delete INV-1042">🗑</button>
</div>
</td>
</tr>
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Action | Visual | Screen reader |
|---|---|---|
| Tab into the row’s actions | Actions appear | “Edit INV-1042, button” |
| Tab past the last action | Actions fade | Next row’s link |
| Grid mode: arrow to actions cell | Actions appear | Cell content with buttons |
| Browse mode | No change | Buttons read as part of the row |
| Touch | Always visible | Buttons reachable by swipe |
Step-by-step implementation
Permalink to "Step-by-step implementation"- Render actions with every row. If performance is a concern in large tables, render them in virtualized rows only, but never on hover.
- Hide with opacity. Opacity keeps buttons focusable and in the accessibility tree; display none and visibility hidden remove them.
- Reveal on focus-within. Add
:focus-withinon the row so any focus inside — the row link or an action — shows the actions. - Show on touch. Use
hover: noneorpointer: coarsemedia queries to keep actions visible on touch devices. - Consider an actions menu. If there are more than three actions, collapse them into one always-visible “Actions for INV-1042” menu button.
Integration context
Permalink to "Integration context"Menus for row actions follow row action menus with the menu button pattern, and naming follows naming icon-only row action buttons. Primary actions are covered in split buttons for primary row actions. Touch sizing is in touch targets in mobile data tables.
Gotchas
Permalink to "Gotchas"Tab order bloat. Three actions per row in a 50-row table add 150 tab stops; in a grid, use roving tabindex so actions are reached by arrow keys within the row instead.
Focus visibility during transitions. A fade-in on focus can leave a focused button nearly invisible for a moment; keep transitions short.
Tooltip on icons. Icon actions need tooltips on hover and focus, which must themselves follow 1.4.13.
Edge cases
Permalink to "Edge cases"- Selected rows. Keep actions visible on the selected row so users can act on it after moving the pointer away.
- Open menus. When an action opens a menu, keep the row’s actions visible until the menu closes.
- Screen magnification. Magnifier users may not see actions appear at the far right; consider placing them near the row start.
Worked example
Permalink to "Worked example"A content management table revealed edit and delete icons on row hover, rendering them in a mouseenter handler. Keyboard users could open items through the title link but could not delete anything; screen reader users never knew the actions existed. The team rendered actions in every row, switched to opacity with :focus-within, showed them always on touch and named each button with the row’s title. An audit for 2.1.1 and 2.4.7 then passed.
Common mistakes
Permalink to "Common mistakes"- Rendering actions on mouseenter.
- display: none for the hidden state.
- No touch fallback.
- Unnamed icon buttons.
Testing checklist
Permalink to "Testing checklist"Testing it with a screen reader
Permalink to "Testing it with a screen reader"In NVDA browse mode, arrow through a row and confirm the action buttons are read with their row-specific names. Switch to focus mode and tab into the row; sighted observers should see the actions appear as the first one receives focus. On iOS with VoiceOver, swipe through a row and confirm the actions are reachable and visible. Finally, unplug the mouse and complete an edit and a delete using only the keyboard.
Performance notes
Permalink to "Performance notes"Rendering actions in every row is cheap when they are simple buttons, but heavyweight tooltips or menus per row add up. Render the buttons eagerly and create tooltips and menus lazily on first focus or hover. In virtualized grids, actions are only rendered for visible rows anyway. Avoid per-row event listeners by delegating click and keydown handling to the table and reading the row identifier from a data attribute.
Design system notes
Permalink to "Design system notes"Provide a row actions slot in the table component with focus-within reveal and touch fallback built in, and a lint rule against hover-only rendering.
FAQ
Permalink to "FAQ"Is opacity zero a problem for screen readers?
No. Screen readers ignore opacity; the buttons remain available.
Should hidden actions be in the tab order?
In simple tables, yes; in grids, reach them with arrow keys.
Can I reveal actions only on row focus?
Focus-within covers both the row and its children; use it.
Are hover-revealed actions discoverable?
Less so; pair them with an always-visible actions button for important actions.
What about context menus?
Right-click menus need keyboard equivalents; see the context menu guide.
Do revealed actions need 1.4.13 compliance?
Yes, if they appear on hover or focus, they must stay while the pointer or focus remains.
Related
Permalink to "Related"- Row action menus with the menu button pattern — collapsing actions
- Split buttons for primary row actions — primary plus more
- Naming icon-only row action buttons — names