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.
Revealing row actions for every input Flow: pointer hover, keyboard focus within the row, or a touch device each reveal the row's actions; the actions were in the DOM all along; focus can land on them directly. Revealing row actions for every inputPointer hover:hover revealsKeyboard focus in row:focus-within revealsTouch devicealways visibleActionsin DOM, named, focusable
Hover is one trigger among several.

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.

Hover-only versus focus-aware reveal Comparison: hover-only reveal hides actions from keyboard and touch users; focus-within reveal with actions always in the DOM serves everyone. Hover-only versus focus-aware reveal✗ Rendered on mouseenter:hover onlyInvisible focused buttonsNothing on touch✓ Always in DOM:hover and :focus-withinVisible when focusedVisible on coarse pointers

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
Reveal techniques and their effects Matrix of hiding techniques — rendered on hover, display none, visibility hidden, opacity zero with focus-within — against keyboard reach, screen reader presence and focus visibility. Reveal techniques and their effectsKeyboard reachIn accessibility treeVisible when focusedRendered on hoverNoNoNodisplay: noneNoNoNovisibility: hiddenNoNoNoopacity: 0 + focus-withinYesYesYes
Opacity with focus-within is the safe combination.

Step-by-step implementation

Permalink to "Step-by-step implementation"
  1. Render actions with every row. If performance is a concern in large tables, render them in virtualized rows only, but never on hover.
  2. Hide with opacity. Opacity keeps buttons focusable and in the accessibility tree; display none and visibility hidden remove them.
  3. Reveal on focus-within. Add :focus-within on the row so any focus inside — the row link or an action — shows the actions.
  4. Show on touch. Use hover: none or pointer: coarse media queries to keep actions visible on touch devices.
  5. 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.

Permalink to "Related"

← Back to Row Actions & Context Menus