Accessibility

What Menu does for you, and what it leaves to the page around it.

What Menu renders

Menu renders the ARIA menu pattern, and the kit changes none of it. The tree below tells you which parts are left to you.
  • The root is a <ul role="menu"> carrying tabIndex={0} and a generated id.
  • Each item is an <li role="menuitem"> with tabIndex={-1} and aria-disabled.
  • A submenu title is a <div role="menuitem"> carrying aria-haspopup, aria-expanded and aria-controls pointed at its own list, which is another role="menu".
  • A group renders its wrapper and its heading as role="presentation" and only the list beneath as role="group". A divider is role="separator".
  • Nothing tells assistive tech which item is current. aria-selected is only written when an item's role is option, so the selection is a class and a color. Put aria-current="page" on the link inside the current item yourself.

Keyboard

The whole menu is one tab stop: the root is focusable and every item is tabIndex={-1}, so Tab moves onto the menu and past it, and the arrow keys move inside it. Which arrow does what depends on the mode.
  • inline: Up and Down move between items. Enter opens or closes a submenu in place, without moving into it.
  • vertical: Up and Down move between items, Enter or Right opens the branch and moves into it, Left or Escape goes back to the parent.
  • horizontal: Left and Right move along the top level, Down or Enter opens a branch and moves into it. Inside the open popup the vertical map takes over.
  • Left and Right are swapped under direction="rtl", so “into the submenu” stays the direction the submenu opens.
  • Home and End move to the first and last focusable item at the current level, which is the popup you are in, not the root.
  • The arrow keys, Home and End all call preventDefault(), so the page underneath does not scroll while the menu has focus.
  • When an item contains an <a href>, focus goes to the link instead of the row, so Enter follows the route with no key handler of your own. One more reason to put a real link in the label.
role="menu" and role="menuitem" are the ARIA pattern for a list of commands: an application menu bar, or what a Dropdown opens. A sidebar of destinations is a set of links, and its pattern is a <nav> around a list. Menu renders the menu roles either way, so a site navigation built from Menu announces itself as a command menu.
  • Put a real <a href> inside each item's label. It restores the browser affordances the roles took away: open in a new tab, copy the address, the status bar preview.
  • Wrap the menu in <nav aria-label="…"> so a reader jumping by landmark can find it and tell it from the other menus on the page.
  • You can override the root's role by passing one, but treat that as a last resort. The keyboard behavior is built for the menu pattern, and changing the role does not change the keys.
  • Reserve the command reading for what it is for: the menu inside a Dropdown, and any Menu you have turned into an actions list with selectable={false}.

Naming the menu and its items

  • The menu itself has no name until you give it one. Pass aria-label on <Menu> and it lands on the role="menu" element. Name it for what it navigates (“Workspace settings”), not for what it is (“Menu”).
  • A page with more than one menu needs more than one name. Two unnamed menus are indistinguishable in a landmark or element list.
  • An item's name is its label's text. An icon is decoration beside it and names nothing on its own.
  • An item's title is the DOM title attribute and the collapsed tooltip's fallback text. It is not an accessible name, and a title attribute is not read on touch at all.
  • An icon-only item, in a collapsed rail or anywhere else, still needs its word: keep the text in the label and hide it with sr-only rather than leaving the label out.

Disabled items cannot be reached

A disabled item is rendered with aria-disabled and without a tabIndex, which takes it out of the list the arrow keys walk. A keyboard reader never lands on it, so they never hear that it exists or that it is unavailable.
  • Prefer leaving an unavailable entry out over disabling it, unless its absence would change what the rest of the menu means.
  • Where it has to stay, keep it enabled and let the destination explain why it is empty. A sentence on the page beats a row nobody can reach.
  • Never let dimming be the only signal. Grayed ink is a contrast difference, and a contrast difference on its own is not a message.

Horizontal overflow

In mode="horizontal" the menu moves whatever does not fit into one trailing submenu titled by overflowedIndicator. That entry is a real submenu, so it carries aria-haspopup and the arrow keys reach it, but its accessible name is whatever you put in the title.
  • The kit fills the title with your icon pack's more icon, which has no text. Pass your own node with a word in it, or add a visually hidden word beside the icon.
  • Which items are hidden depends on the viewport, so a keyboard reader on a narrow window meets a different menu from the one you tested. Test the narrow one.
  • disabledOverflow keeps every item in the row. Nothing moves into a popup, and nothing stops the row from clipping either.

Contrast and motion

  • The current item is carried by ink color and a background tint, and neither is a shape. On a rail where several items are already colored, add something that is not color: a rule down the active edge, a weight change, an icon state.
  • theme="dark" is a second palette on the same page. Check the dark menu's own pairs: resting text on the menu, selected text on the selected fill, and the hover state between them.
  • Every color here comes from the active theme, so contrast depends on the theme. A custom seed can fall under 4.5:1 with nothing warning you, so check yours. The Tokens tab lists which tokens Menu reads.
  • Each mode has its own motion: a slide for horizontal popups, a height collapse for inline submenus, a zoom for the rest. Pass motion to replace all of them.
  • An inline submenu opening animates the height of a list, a larger movement than a fade. For readers who ask for reduced motion, turn it off with the theme's token: { motion: false } or by passing motion yourself.