UIDex

Menu (Dropdown Menu and Context Menu)

قائمة الأوامر

role="menu" + role="menuitem"

also calleddropdown menu، context menu، right-click menu، action menu، overflow menu، kebab menu، منيو، قائمة السياق، قائمة كليك يمين، قائمة الثلاث نقاط، قائمة الأوامر المنسدلة، دروب داون منيو

A menu is a small panel that opens over the interface and lists commands ready to run; pressing a row fires the command and the panel closes. It comes in two shapes built from the same parts: a dropdown menu opened by a visible button such as a three dot control, and a context menu opened by the right mouse button or the ContextMenu key on whatever the user is standing on. No row in it stores a value, which is why menuitem does not support aria-selected at all. It is not a select or a listbox: a select holds a value that stays on the trigger and submits with the form, a listbox marks its choice with aria-selected and keeps it after closing, and the split is what happens on press, running a command against setting a value something reads later. It is not a popover either, because a popover is a floating box that takes any content with no semantics of its own, while role="menu" is a strict contract: menuitem-shaped children, arrow-key navigation, and one Tab stop for the whole thing. And it is not a list of links: site navigation is a <ul> of <a> elements, and role="menu" on that list with role="menuitem" on the anchors strips the link role, so they drop out of the screen reader links list and the page promises arrow-key navigation that nobody wrote. Keep role="menu" for application commands such as Edit and Sort by, and leave links as links inside <nav>.

If you called it…

"the three dots that open a list of actions""the thing that appears when I right click""the little list with copy and delete in it""a menu with another menu coming out of its side""the panel under the button that closes as soon as I pick something"

Live specimen

Interact with the demo. Every part is real and numbered.

تقرير-الربع.pdf

اضغط بالزرّ الأيمن في أي مكان هنا لفتح قائمة السياق.

آخر أمر: فتح · الشبكة: ظاهرة

RTLالقائمة الفرعية تفتح نحو نهاية السطر، وسهمها وحده هو ما ينعكس

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a command menu in both shapes from one structure: a dropdown menu from a button, and a context menu from the contextmenu event. The trigger is a real button with aria-haspopup="menu", a toggling aria-expanded, and an aria-controls pointing at the menu id; it opens on Enter, Space, ArrowDown onto the first item, and ArrowUp onto the last. The container is role="menu" named by aria-labelledby, and its direct children are menuitem, menuitemcheckbox with aria-checked, menuitemradio inside a role="group", or separator, with no role-less element in between. Make the menu one Tab stop: a single item at tabindex="0" and the rest at -1, arrow keys that really move focus, Home and End to the ends, and first-letter type-ahead that normalises Arabic labels by stripping diacritics and folding the alef forms. Escape closes and returns focus to the trigger, and so do an outside press and Tab. A submenu is an item with aria-haspopup="menu" followed by a role="menu" container inside a role="none" wrapper; it opens toward the logical end of the row, flips at the viewport edge, keeps a safe triangle so diagonal pointer travel does not close it, and never grows a third level. For the context menu call preventDefault, support the ContextMenu key and Shift+F10, and place it logically: clientX is always measured from the physical left edge of the window, so under rtl convert it to an inset-inline-start of documentElement.clientWidth minus clientX. Use logical properties throughout: padding-inline-start for the mark column, margin-inline-start: auto for the shortcut, text-align: start for the label, and never left, right, or ml-auto. Mirror the submenu arrow through one dedicated class, and isolate the shortcut text with dir="ltr" and unicode-bidi: isolate.

How it behaves right-to-left

This section is ours alone.

Which side the submenu opens on

mirrors

A submenu follows the reading direction: it opens to the right of its parent row in English and to the left in Arabic, which is the logical end of that row. Compute the side when the menu opens instead of writing it once in CSS, because it has to flip to the opposite side as soon as the remaining viewport width runs out. Floating UI is logical in its alignment only: the -start and -end suffixes read direction off the element, while the main side in placement is spelled physically (top, right, bottom, left), so either you pick "left-start" yourself under rtl, or you hand the job to a component that reads direction, such as Radix, where dir="rtl" belongs on DropdownMenu.Root or on a DirectionProvider around the tree and never on DropdownMenu.Sub, which takes no dir prop.

The submenu arrow

mirrors

The arrow at the end of the row points at the side the submenu will open on, so it is purely directional and has to flip with the page. If you draw it as a text character, do not assume the bidi engine will handle it: Bidi_Mirrored is Yes for › (U+203A), which mirrors on its own, and No for → (U+2192) and ▸ (U+25B8), which stay exactly as drawn whatever the direction is. Put the flip in a dedicated class such as .flip-x { transform: scaleX(-1) } under [dir="rtl"] and apply it to this arrow alone, because a blanket rule on svg mirrors the checkmark and the copy and delete glyphs along with it.

The mark column and the shortcut column

mirrors

Every row is three slots: the state mark at the logical start, the label in the middle, and the shortcut hint at the logical end, so the two outer slots trade places with page direction. Reserve the mark slot on every row with padding-inline-start or an empty element of the same width, and push the shortcut out with margin-inline-start: auto. The ready-made trap here is the DropdownMenuShortcut component in shadcn/ui: its class is ml-auto, which works in English and leaves the shortcut glued to the wrong side in Arabic until you swap it for ms-auto.

The shortcut text itself

never mirrors

The slot moves with the page, but the order inside the run never changes: Ctrl+K reads the same in both directions. The risk sits in the symbols that lead it, because ⌘ (U+2318) and ⇧ (U+21E7) carry bidi class ON, meaning neutral, and a neutral standing at the edge of a run takes the direction of the paragraph around it, hops to the other side of the letter, and the eye reads K⌘ instead of ⌘K. Isolate the run as one block with dir="ltr" plus unicode-bidi: isolate, or wrap it in <bdi>, rather than patching it with an extra space or a control character typed into the string by hand.

ArrowRight and ArrowLeft

mirrors

The two keys trade jobs with direction: in Arabic ArrowLeft opens the submenu and ArrowRight closes it, and in English it is the other way round. Read the direction once from getComputedStyle(el).direction and derive openKey and closeKey from it, instead of scattering an rtl check through every branch of the key handler. Nothing else moves: ArrowDown, ArrowUp, Home, and End work on DOM order, Escape closes and restores focus, and first-letter type-ahead has no relationship to direction at all.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIArole="menu" + role="menuitem"The contract itself: menu children are items, separators, or groups, and nothing else.
ARIAaria-haspopup="menu" + aria-expandedThey live on the trigger, not the menu; a value of true implies menu, and naming it is clearer.
HTML<menu>The HTML element with that name is a <ul> under another spelling; its implicit role is list, not menu.
Radix UI<DropdownMenu.Root> / <ContextMenu.Root>One structure with two triggers, and the dir prop flips the submenu side and the meaning of the horizontal arrows.
shadcn/ui<DropdownMenuCheckboxItem> / <DropdownMenuShortcut>The first hands you aria-checked ready made; the second is a span with ml-auto that you need to change to ms-auto.
Material UI<Menu> + <MenuItem>Renders a ul with role="menu" and moves real DOM focus onto the item instead of using aria-activedescendant.
CSSunicode-bidi: isolateIsolates the Latin shortcut run so the ⌘ symbol does not jump to the other side of the letter.

See also

Updated · 2026-08-19