Radix Dropdown Menu for Row Actions

Permalink to "Radix Dropdown Menu for Row Actions"

Radix UI’s DropdownMenu implements the ARIA menu button pattern: a trigger with aria-haspopup="menu" and aria-expanded, a menu with role="menu", arrow-key navigation, typeahead, Escape to close and focus returned to the trigger. In data tables, it is a natural choice for per-row action menus (“…” buttons). The library handles behaviour; the accessibility work is naming triggers per row, choosing item types correctly, handling destructive actions, and telling users what happened after an action.

This guide belongs to headless UI primitives for data.

Spec reference

Permalink to "Spec reference"
  • Menu button pattern — trigger button with aria-haspopup="menu" and aria-expanded; role="menu" with menuitem, menuitemcheckbox, menuitemradio.
  • Radix DropdownMenu — Root, Trigger, Portal, Content, Item, CheckboxItem, RadioGroup, Separator, Sub; keyboard: arrows, Home/End, typeahead, Escape, Enter/Space.
  • Focus — focus moves into the menu on open and returns to the trigger on close.
  • Relevant criteria: SC 4.1.2, 2.1.1, 2.4.3 (A), SC 4.1.3 (AA) for outcomes.
Opening and using a row action menu Sequence among user, trigger, menu and status: Enter on "Actions for INV-1042" opens the menu with focus on the first item; Down moves to Archive; Enter archives; the menu closes, focus returns to the trigger and the status announces the result. Opening and using a row action menuUserTriggerMenuStatusEnter on "Actions forINV-1042"open; focus "Open"Down to "Archive", Enterclose; focus returns"INV-1042 archived. Undo available."
Radix provides open, navigate and return; you provide names and the outcome message.

When to use — and when not to

Permalink to "When to use — and when not to"

Use DropdownMenu for row actions with three or more options. For one or two actions, visible buttons in the row are simpler and faster. For actions that apply to many rows, use a bulk toolbar instead.

Do not put form inputs or complex content in a menu; use a popover or dialog. Do not trigger menus only on right-click; ContextMenu should have a keyboard equivalent (Shift+F10 or the visible trigger).

The misapplication: every row’s trigger named “More actions”, producing dozens of identically named buttons in the tab order.

Generic triggers versus record-named triggers Comparison: generic "More actions" triggers are indistinguishable in lists of buttons; triggers named with the record make each one meaningful out of context. Generic triggers versus record-named triggers✗ "More actions" × 50Indistinguishable in button listsVoice control: "click more actions" ambiguousMenu context unknown✓ "Actions for INV-1042"Unique and meaningfulSpecific command possibleMenu labelled by the trigger

Annotated code example

Permalink to "Annotated code example"
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';

function RowActions({ invoice, onArchive, onDuplicate }) {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button type="button" className="icon-btn" aria-label={`Actions for ${invoice.id}`}>   {/* SC 4.1.2 */}
          <DotsIcon aria-hidden="true" />
        </button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content align="end" sideOffset={4}>               {/* labelled by the trigger */}
          <DropdownMenu.Item onSelect={() => openInvoice(invoice)}>Open</DropdownMenu.Item>
          <DropdownMenu.Item onSelect={() => onDuplicate(invoice)}>Duplicate</DropdownMenu.Item>
          <DropdownMenu.CheckboxItem checked={invoice.flagged} onCheckedChange={(v) => flag(invoice, v)}>
            Flag for review                                            {/* menuitemcheckbox with state */}
          </DropdownMenu.CheckboxItem>
          <DropdownMenu.Separator />
          <DropdownMenu.Item onSelect={() => confirmArchive(invoice)}>Archive…</DropdownMenu.Item>  {/* ellipsis: opens a dialog */}
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  );
}

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Key Behaviour Expected announcement
Enter / Space / Down on trigger Open; focus first item “Actions for INV-1042, menu, Open, 1 of 4”
Up / Down Move Item names
Typing Typeahead Matched item
Enter / Space on item Activate; close Outcome via status
Space on checkbox item Toggle “Flag for review, checked”
Escape Close; focus to trigger Trigger name
Tab Close and move on Next element
Radix item types and when to use them Matrix of item types — Item, CheckboxItem, RadioItem, Sub, Separator — against role exposed, use case and what to add. Radix item types and when to use themRoleUse forYou addItemmenuitemActionsOutcome messagesCheckboxItemmenuitemcheckboxTogglesState in label if neededRadioItemmenuitemradioOne-of choicesGroup labelSubmenuitem with submenuNested actionsKeep shallowSeparatorseparatorGroupingNo
Choose item types by what the action does, not by appearance.

Integration context

Permalink to "Integration context"

The menu button pattern itself is covered in row action menus with the menu button pattern. Destructive actions follow confirming and undoing destructive row actions. Portalling and focus order are discussed in anchor positioning and focus order for overlays.

Gotchas

Permalink to "Gotchas"

Triggers inside grids. In a role="grid", the trigger should not be an extra tab stop; manage it within the grid’s focus model (Enter on the cell, or tabIndex={-1} with grid navigation).

Rows removed by actions. Archiving removes the row and its trigger; Radix returns focus to a trigger that no longer exists. Move focus explicitly to the next row.

Portals. DropdownMenu.Portal renders at the end of the body; Radix manages focus return, but reading order differs. Test with screen readers in browse mode.

Worked example

Permalink to "Worked example"

An admin table had 50 rows, each with a “More actions” trigger. Screen reader users navigating by buttons heard “More actions” 50 times. The team named triggers “Actions for {record}”, used CheckboxItem for “Flag for review”, added an ellipsis and confirmation dialog for Archive, moved focus to the next row after archiving and announced “INV-1042 archived. Undo available.” Voice control users could now say “click actions for INV-1042”.

Testing it with a screen reader

Permalink to "Testing it with a screen reader"
  1. Navigate by buttons (B in NVDA); confirm each trigger is named with its record.
  2. Open a menu with Enter; confirm the menu name and first item.
  3. Use arrows and typeahead; confirm item names and checkbox states.
  4. Activate an item that removes the row; confirm focus lands on the next row and the outcome is announced.
  5. Press Escape; confirm focus returns to the trigger.

Wrapping it for product teams

Permalink to "Wrapping it for product teams"

A RowActions wrapper takes recordName and an actions array with kinds (action, toggle, destructive). It renders correctly named triggers, uses CheckboxItem for toggles, routes destructive actions through a confirmation dialog, announces outcomes and moves focus when rows disappear. Teams declare actions; the wrapper handles accessibility.

Common mistakes

Permalink to "Common mistakes"
  • Generic trigger names.
  • Using Item for toggles instead of CheckboxItem.
  • No outcome announcements.
  • Focus lost when the action removes the row.

Adoption checklist

Permalink to "Adoption checklist"
  1. Wrap DropdownMenu in a RowActions component requiring a record name.
  2. Provide item helpers for actions, toggles and destructive actions (with confirmation).
  3. Connect outcomes to the shared announcer.
  4. Handle focus after row removal centrally.
  5. Test inside grids with roving focus.

FAQ

Permalink to "FAQ"
Does Radix DropdownMenu follow the ARIA menu pattern?

Yes; it implements the menu button pattern including keyboard handling and focus management.

Should menus be portalled?

Radix portals by default and manages focus. If reading order matters for your users, render inline where possible.

How do I show keyboard shortcuts in items?

Render the shortcut text visually and add aria-keyshortcuts to the item if the shortcut works globally.

Can I use ContextMenu for right-click?

Yes, but also provide the visible trigger and Shift+F10 support so keyboard users can reach the same actions.

How do I name the menu?

Radix labels the content by the trigger; a trigger named with the record names the menu too.

How do I handle disabled actions?

Use the disabled prop; disabled items are skipped or announced as unavailable. Explain why in the label if it helps.

Should destructive items be red?

Colour can help, but use text (“Archive…”, “Delete invoice…”) and confirmations; never colour alone.

Should every row have a menu trigger in the tab order?

In a semantic table, yes, each trigger is a tab stop; name them well. In an interactive grid, keep triggers inside the grid’s focus model instead.

How do I add submenus accessibly?

Radix Sub components open with Right arrow and close with Left; keep nesting to one level and name submenu triggers clearly (“Move to…”).

Permalink to "Related"

← Back to Headless UI Primitives for Data