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"- Names for icon-only triggers: naming icon-only row action buttons.
- Focus rules when the acted-on row disappears: preserving focus when rows are deleted.
- Modal dialogs: native dialog versus custom focus traps.
- A grid’s focused cell, for context menus: implementing roving tabindex for custom data grids.
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.`);
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 |
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.
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.
Related
Permalink to "Related"- Names & descriptions — naming triggers and menus
- Keyboard shortcuts & command palettes — faster routes to the same actions
- Focus management in single-page apps — focus after actions
- Bulk selection & batch actions — actions on many rows
- Inline editing & form controls — actions inside cells