Skip to content

Menu

Get Ritla UI

A panel of rows that opens from a trigger: one choice, several choices, or submenus two levels deep. It opens at the trigger's start, the right in Arabic, and submenus open toward the inline end.

Preview

Mirrored is the same English content laid out right to left: it separates a mirroring bug from a translation one.

PreviewCode
EnglishMirroredArabic
LTR →

Installation

  1. Add the component

    npx ritla add menu

    Also works with shadcn: npx shadcn@latest add @ritla/menu

  2. Dependencies it brings

    @base-ui/react@phosphor-icons/react

  3. Or copy by hand

    Installing by hand copies the component's source into your project. The source comes with a Ritla UI license, so the CLI is the short way to the same files.

Usage

menu.tsx
import { Button } from "@/components/ui/button";import { Menu, MenuContent, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "@/components/ui/menu";<Menu>  <MenuTrigger render={<Button variant="outline" />}>This month</MenuTrigger>  <MenuContent>    <MenuRadioGroup value={period} onValueChange={setPeriod}>      <MenuRadioItem value="week">This week</MenuRadioItem>      <MenuRadioItem value="month">This month</MenuRadioItem>    </MenuRadioGroup>  </MenuContent></Menu>

Examples

Multi select

Type = Multi select: checkbox rows with a second line; the menu stays open while you toggle. The group's label names it for screen readers.

EnglishMirroredArabic
LTR →

Multi level

Type = Multi level with a submenu open two levels deep: each level opens at the end of its row, to the left in Arabic, where ArrowLeft opens it.

EnglishMirroredArabic
LTR →

API reference

Menu

The root: holds the state.

PropTypeDefaultDescription
open / defaultOpenbooleanNoneWhether the menu is open (controlled or not).
onOpenChange(open: boolean) => voidNoneCalled when the menu opens or closes.

MenuContent

The panel, shown on a layer above the page.

PropTypeDefaultDescription
align"start" | "center" | "end""start"Alignment with the trigger; Figma uses the inline start.
sideOffsetnumber12The gap from the trigger, in px.

MenuRadioGroup, MenuRadioItem

Type = Single select: one choice; the chosen row has a Check at the inline end.

PropTypeDefaultDescription
value / defaultValue / onValueChangestringNoneMenuRadioGroup: the current choice.
valuestringNoneMenuRadioItem: this row's value.

MenuCheckboxItem

Type = Multi select: a row that toggles.

PropTypeDefaultDescription
checked / defaultChecked / onCheckedChangebooleanNoneWhether it's on.
descriptionReactNodeNoneA second line under the title.

MenuSubmenu, MenuSubmenuTrigger, MenuSubmenuContent

Type = Multi level: a row that opens another menu, which can hold a submenu in turn.

PropTypeDefaultDescription
childrenReactNodeNoneMenuSubmenuTrigger: the title; a caret is added at the inline end.

MenuItem, MenuLinkItem, MenuGroup, MenuLabel, MenuSeparator

The same rows and sections as on Menu Button, which lists the icon, second line, count and destructive props.

PropTypeDefaultDescription

Accessibility

Keyboard

Keys are written for left-to-right. In right-to-left the arrow keys swap: the one pointing toward the reading direction moves forward.

KeyAction
EnterSpaceArrowDownOn the trigger: opens the menu and focuses the first row.
ArrowDownArrowUpMoves between rows and wraps.
ArrowRightArrowLeftOpens and closes a submenu. Flips in Arabic: ArrowLeft opens.
SpaceEnterOn a choice: picks or toggles it, and the menu stays open.
EscapeCloses one level; from the first level it returns focus to the trigger.

ARIA

  • The menu is role="menu"; choices are menuitemradio and menuitemcheckbox with aria-checked, so selection never rests on the Check alone.
  • A row that opens a submenu carries aria-haspopup and aria-expanded.
  • MenuLabel names its group, so screen readers announce the section.

Arabic and RTL notes

Mirrors

  • The menu opens at the trigger's start (the right in Arabic), and each submenu toward the end of its row (the left).
  • The submenu caret flips, and arrow keys follow the reading direction.
  • The menu takes the trigger's direction and language, even inside an Arabic island on an English page.

Does not mirror

  • The Check on a chosen row never mirrors, though it sits at the inline end.
  • Latin names such as PDF and CSV stay left to right.
Numerals
Counts in rows and on the trigger use formatNumber, so they follow the numerals setting.
Arabic typography
Titles are label-14 Medium, second lines label-12, section headers body-14 Semibold. Rows are 48px and grow for Arabic.
Mixed-direction text
A Latin name at the end of an Arabic row (“تنزيل PDF”) renders correctly; in the middle, wrap it in <bdi>.

Checks it passes

The Ritla checks this component satisfies by construction. Used as documented, it does not bring these defects onto your page.

  • R050Arabic text falling back past the declared font family (measured, declared, or a webfont that failed to load)
  • R051letter-spacing applied to Arabic text (breaks the connected script)
  • R042Clipped text: content wider than its box with overflow hidden and no ellipsis
  • R043Fixed-pixel-width container whose Arabic text exceeds the space