Row Actions & Context Menus

Permalink to "Row Actions & Context Menus"

A data table is where users look; row actions are how they work. Edit, duplicate, export, assign, archive, delete — each row carries some combination, delivered through visible buttons, a “⋯” menu, a right-click context menu, a toolbar above the grid, or all four. These are also where keyboard and screen reader users most often get stuck: menus that open only on hover, context menus that ignore Shift+F10, toolbars that cost ten Tab presses per visit, and delete buttons with no safety net that a stray Enter can trigger.

This topic covers the four delivery mechanisms and the safety layer under them. It is written for engineers building table and grid interactions, and for design system maintainers whose RowActions, Menu, Toolbar and ConfirmDialog components decide how accessible every product’s actions are.

It belongs to core ARIA & keyboard navigation for data UIs.

WCAG criteria in scope

Permalink to "WCAG criteria in scope"
Criterion Level Relevance to row actions and menus
2.1.1 Keyboard A Every action — including right-click menus — is operable by keyboard
2.4.3 Focus Order A Focus moves into menus, returns to triggers, and lands deliberately after actions
4.1.2 Name, Role, Value A Menu buttons expose aria-haspopup and aria-expanded; items have names and roles
2.4.6 Headings and Labels AA Triggers and confirmations name the record and the action
3.3.4 Error Prevention (Legal, Financial, Data) AA Destructive data actions are reversible or confirmed
2.2.1 Timing Adjustable A Undo is not limited to a short, fixed window
4.1.3 Status Messages AA Results and undo instructions are announced without moving focus

Prerequisites

Permalink to "Prerequisites"
Four ways to deliver row actions, one safety layer Layers of row action delivery: visible row buttons, the row menu button, the context menu, and the grid toolbar, all sitting on a shared layer of confirmation, undo and focus rules. Four ways to deliver row actions, one safety layerVisible row buttonsone or two primary actions, named with the recordRow menu button"Actions for INV-1042" — the menu button patternContext menucontextmenu event: right-click, Shift+F10, Menu keyGrid toolbargrid-level actions, one tab stop, arrow keysSafety and focusundo or confirm; deliberate focus; one status region
Different entry points, one action framework underneath — so every route behaves the same.

ARIA & HTML spec reference

Permalink to "ARIA & HTML spec reference"
Element / attribute Valid values When to apply Common misuse
aria-haspopup menu, true, dialog, … On a button that opens a menu or dialog On links; on buttons that just toggle a section
aria-expanded true, false On the menu button, reflecting the menu’s state Never updated when the menu closes
role="menu" / menuitem — Action menus Navigation link lists; items containing buttons
role="separator" — Between item groups in a menu Styled <hr> without a role inside role="menu"
role="toolbar" with aria-label, aria-controls Five or more grid-level controls The role without arrow-key behaviour
contextmenu event — Custom context menus mousedown with button === 2
<dialog> + autofocus — Confirmations, focus on the safe option Focus on the destructive button

Step-by-step implementation

Permalink to "Step-by-step implementation"

Step 1 — Split primary and secondary actions (SC 2.1.1, 2.4.6)

Permalink to "Step 1 — Split primary and secondary actions (SC 2.1.1, 2.4.6)"
<td>
  <button type="button">Open<span class="visually-hidden"> INV-1042</span></button>
  <button type="button" aria-haspopup="menu" aria-expanded="false">
    <span aria-hidden="true">⋯</span><span class="visually-hidden">Actions for INV-1042</span>
  </button>
</td>

Step 2 — Implement the menu button (SC 4.1.2, 2.4.3)

Permalink to "Step 2 — Implement the menu button (SC 4.1.2, 2.4.3)"
btn.setAttribute('aria-expanded', 'true'); menu.hidden = false; firstItem.focus();
// Escape: close and btn.focus(); Tab: close and let focus move on

Step 3 — Open context menus from the right event (SC 2.1.1)

Permalink to "Step 3 — Open context menus from the right event (SC 2.1.1)"
grid.addEventListener('contextmenu', (e) => { e.preventDefault(); openAt(focusedCell()); });

Step 4 — Toolbar for grid actions (SC 2.4.3)

Permalink to "Step 4 — Toolbar for grid actions (SC 2.4.3)"
<div role="toolbar" aria-label="Invoice table actions" aria-controls="inv-grid">…</div>

Step 5 — Undo or confirm destructive actions (SC 3.3.4, 2.2.1, 4.1.3)

Permalink to "Step 5 — Undo or confirm destructive actions (SC 3.3.4, 2.2.1, 4.1.3)"
status(`${id} archived. Press Control Z to undo.`);
One row action, end to end Flow of a row action from trigger through menu, action framework decision, execution and focus with announcement. One row action, end to endTriggerbutton, menu,context menuChoose itemarrows, EnterFrameworkundo or confirm?Executerow changesFocus + statusdeliberate target,one message
The route in can vary; the route out — safety, focus, message — should always be the same.

Keyboard interaction contract

Permalink to "Keyboard interaction contract"
Key Context Action Expected AT announcement Failure indicator
Tab Row Reach row buttons and menu trigger “Actions for INV-1042, menu button, collapsed” “button” with no name
Enter / Down Arrow Menu trigger Open menu, focus first item “Edit, menu item, 1 of 4” Menu opens, focus stays on trigger
Shift+F10 / Menu key Grid cell Open context menu at cell “Actions for Northwind, Amount, menu” Browser menu appears
Right Arrow Toolbar Next control “Columns, menu button” Nothing
Escape Menu or dialog Close, return focus Trigger re-read Focus on body
Ctrl+Z Grid, after undoable action Undo “Archive of INV-1042 undone.” Nothing, or browser undo in a field

Screen reader compatibility matrix

Permalink to "Screen reader compatibility matrix"
AT + browser Menu button Context menu via keyboard Toolbar
NVDA + Chrome “menu button, collapsed”; items with position Shift+F10 in focus mode “toolbar” announced on entry
NVDA + Firefox Same Same Same
JAWS + Chrome “menu, button”; submenu state read Shift+F10 or Menu key Announced
VoiceOver + Safari “menu pop-up button” VO+Shift+M “toolbar”
TalkBack + Chrome “menu, button” Long-press varies — provide the visible menu Announced
Choosing the delivery mechanism Matrix mapping kinds of action — primary per-row, secondary per-row, cell-specific, and grid-level — to the recommended delivery mechanism and its keyboard route. Choosing the delivery mechanismAction kindDeliver asKeyboard routePrimary, per rowVisible buttonTabSecondary, per rowRow menu buttonTab, then arrowsCell-specificContext menu + visible alternativeShift+F10 or menuGrid-levelToolbarTab to bar, arrows along
Most tables need visible primary buttons plus a menu; context menus and toolbars are additions, never the only route.

Edge cases & failure modes

Permalink to "Edge cases & failure modes"

1. Hover-only row actions

Permalink to "1. Hover-only row actions"

Diagnosis: action buttons appear only when the row is hovered, via opacity: 0 or display: none until :hover. Keyboard users tab onto invisible buttons, or cannot reach them at all. Fix: reveal on :focus-within as well as :hover, and never use display: none for the hidden state.

2. The menu trigger that stays “expanded”

Permalink to "2. The menu trigger that stays “expanded”"

Diagnosis: aria-expanded is set to true on open but not reset when the menu closes on outside click. Fix: close through one function that always updates the attribute.

3. Context-menu-only commands

Permalink to "3. Context-menu-only commands"

Diagnosis: “Filter by this value” exists only in the right-click menu. Fix: provide the same command in the column menu or row menu.

4. Confirmation that focuses Delete

Permalink to "4. Confirmation that focuses Delete"

Diagnosis: autofocus on the destructive button; one extra Enter deletes. Fix: focus the safe button and label both buttons with their actions.

5. Undo in a vanishing toast

Permalink to "5. Undo in a vanishing toast"

Diagnosis: a four-second toast with an Undo button that keyboard users cannot reach in time. Fix: announce the undo key, keep the toast until dismissed, and support Ctrl+Z.

Who depends on these patterns

Permalink to "Who depends on these patterns"

Keyboard-only users — people with motor impairments, repetitive strain injuries, or simply a preference for the keyboard — reach row actions by Tab and arrow keys. Hover-revealed buttons, mouse-only context menus and ten-stop toolbars are the barriers they meet most often in data tables. For them, the number of keypresses per action is the usability metric that matters; the menu button and toolbar patterns exist largely to keep it low.

Screen reader users need to know which record an action affects, what kind of control they are on, and what happened afterwards. “Actions for INV-1042, menu button” answers the first two; the status message after the action answers the third. Without those, a user acting on row 30 of 40 cannot be sure the right invoice was archived.

Speech-input users activate controls by saying their visible names. Row buttons with visible text or tooltips they can say (“click Edit”), and menus whose items have plain names, work well. Icon-only buttons whose accessible names do not start with their tooltip text do not.

Screen magnifier users see a small portion of the table. Context menus that open at the pointer rather than at the focused cell, and toasts in a far corner of the screen, are outside their view. Anchoring menus to focus and announcing results through a status region keeps everything they need near where they are looking.

Users who make mistakes — which is everyone — are served by the safety layer. A specific confirmation and a reachable undo are accessibility features in the plainest sense: they make the interface forgiving.

Row action menus

Permalink to "Row action menus"

Row action menus with the menu button pattern implements the “⋯” menu with aria-haspopup, aria-expanded, arrow keys, type-ahead and focus return — including after the action removes the row.

<button aria-haspopup="menu" aria-expanded="false">Actions for INV-1042</button>

Behaviour note: the menu is named by its trigger, so items can stay short.

Context menus from the keyboard

Permalink to "Context menus from the keyboard"

Opening custom context menus from the keyboard wires menus to the contextmenu event, positions keyboard-opened menus at the focused cell, and keeps a visible route to every command.

grid.addEventListener('contextmenu', onContextMenu);

Behaviour note: VoiceOver’s VO+Shift+M fires the same event — one listener serves everyone.

The toolbar pattern

Permalink to "The toolbar pattern"

The toolbar pattern for grid actions turns a bar of grid controls into one tab stop with arrow-key movement, remembered position and discoverable disabled states — and says when plain tab order is better.

<div role="toolbar" aria-label="Invoice table actions">…</div>

Behaviour note: the role promises arrow keys; ship them or leave the role off.

Confirming and undoing destructive actions

Permalink to "Confirming and undoing destructive actions"

Confirming and undoing destructive row actions chooses between undo and confirmation, writes specific dialogs with safe default focus, and keeps undo reachable without a timer.

<button value="cancel" autofocus>Keep invoice</button>

Behaviour note: tell keyboard users the undo key in the status message itself.

Keyboard stops per row for secondary actions Bar chart comparing the number of tab stops per row for four secondary actions shown as visible buttons against the same actions in one menu button. Keyboard stops per row for secondary actionsFour visible icon buttons4 stops per row — 160 stops in a 40-row tableOne menu button1 stops per row
Four visible buttons per row in a 40-row table is 160 tab stops; one menu button is 40.

Cross-cutting concerns

Permalink to "Cross-cutting concerns"

One action, many routes. The same “Archive” can be reached from a row button, a row menu, a context menu, a toolbar and a keyboard shortcut. Implement it once, in an action framework, and let each route call it. That way the confirmation, undo, focus target and announcement are identical however the user got there.

Focus after actions. Every action ends with a focus decision. Non-destructive actions return focus to where they started. Destructive ones move it to the next row. Actions that open something move focus into it. Write these rules into the framework rather than into each handler.

Announcements. Actions away from focus — bulk archive from a toolbar, undo — need a status message. Route them through the page’s queue so they do not collide with sort or filter messages; see announcement queues for competing messages.

Touch. Long-press context menus are unreliable across mobile browsers and screen readers. Visible menu buttons serve touch users, TalkBack and VoiceOver on iOS alike.

Design system integration

Permalink to "Design system integration"
Component Accessible behaviour it should own Criterion
RowActions Primary buttons + menu button; names composed from the row label 4.1.2, 2.4.6
Menu / MenuButton Keyboard model, aria-expanded sync, focus return, portal positioning 2.1.1, 2.4.3
Grid cellActions Same items for context menu and visible menus; contextmenu listener 2.1.1
Toolbar Roving tabindex, text inputs last, plain order under four controls 2.4.3
Action framework Undo or confirm, focus target, status message, Ctrl+Z 3.3.4, 2.2.1, 4.1.3

Testing checklist

Permalink to "Testing checklist"

Automated

Permalink to "Automated"

Keyboard

Permalink to "Keyboard"

Screen reader

Permalink to "Screen reader"

FAQ

Permalink to "FAQ"
How should row actions be exposed in an accessible data table?

Show the one or two primary actions as visible, named buttons and put the rest in a menu button per row. Custom context menus and toolbars can add routes, but every action should be reachable from a visible control.

How do keyboard users open a right-click menu in a web app?

With Shift+F10 or the Menu key on Windows and VO+Shift+M with VoiceOver on macOS. A custom context menu must listen for the contextmenu event, which all of these fire.

Should row action buttons appear only on hover?

Not only on hover. If you hide them until the row is hovered, also reveal them when the row contains keyboard focus, and hide them with opacity or clipping rather than display none so they stay reachable. Many teams find always-visible actions simpler and more predictable.

Should a row action menu use role="menu" or a list of buttons in a disclosure?

role=“menu” for actions, because users expect arrow-key navigation and a single tab stop there. A disclosure containing ordinary buttons or links is better for navigation-style lists, where arrow keys would surprise users and Tab is the expected way to move.

How many actions should be visible in each row?

One or two that users take on most rows. Put the rest in a menu button, so each row adds only a couple of tab stops and a long table stays quick to move through.

Do destructive row actions need a confirmation dialog?

They need either a confirmation or a way to undo. Undo is less disruptive for reversible actions; confirmation is required for irreversible ones. Both must be fully operable by keyboard.

Permalink to "Related"

← Back to Core ARIA & Keyboard Navigation for Data UIs