Menu
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.
Installation
Add the component
npx ritla add menuAlso works with shadcn:
npx shadcn@latest add @ritla/menuDependencies it brings
@base-ui/react@phosphor-icons/reactOr 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
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.
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.
API reference
Menu
The root: holds the state.
| Prop | Type | Default | Description |
|---|---|---|---|
open / defaultOpen | boolean | None | Whether the menu is open (controlled or not). |
onOpenChange | (open: boolean) => void | None | Called when the menu opens or closes. |
MenuContent
The panel, shown on a layer above the page.
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | "start" | Alignment with the trigger; Figma uses the inline start. |
sideOffset | number | 12 | The gap from the trigger, in px. |
MenuRadioGroup, MenuRadioItem
Type = Single select: one choice; the chosen row has a Check at the inline end.
| Prop | Type | Default | Description |
|---|---|---|---|
value / defaultValue / onValueChange | string | None | MenuRadioGroup: the current choice. |
value | string | None | MenuRadioItem: this row's value. |
MenuCheckboxItem
Type = Multi select: a row that toggles.
| Prop | Type | Default | Description |
|---|---|---|---|
checked / defaultChecked / onCheckedChange | boolean | None | Whether it's on. |
description | ReactNode | None | A second line under the title. |
MenuSubmenu, MenuSubmenuTrigger, MenuSubmenuContent
Type = Multi level: a row that opens another menu, which can hold a submenu in turn.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | None | MenuSubmenuTrigger: 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.
| Prop | Type | Default | Description |
|---|
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.
| Key | Action |
|---|---|
| EnterSpaceArrowDown | On the trigger: opens the menu and focuses the first row. |
| ArrowDownArrowUp | Moves between rows and wraps. |
| ArrowRightArrowLeft | Opens and closes a submenu. Flips in Arabic: ArrowLeft opens. |
| SpaceEnter | On a choice: picks or toggles it, and the menu stays open. |
| Escape | Closes one level; from the first level it returns focus to the trigger. |
ARIA
- The menu is
role="menu"; choices aremenuitemradioandmenuitemcheckboxwitharia-checked, so selection never rests on the Check alone. - A row that opens a submenu carries
aria-haspopupandaria-expanded. MenuLabelnames 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-14Medium, second lineslabel-12, section headersbody-14Semibold. 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.
- Arabic text falling back past the declared font family (measured, declared, or a webfont that failed to load)
- letter-spacing applied to Arabic text (breaks the connected script)
- Clipped text: content wider than its box with overflow hidden and no ellipsis
- Fixed-pixel-width container whose Arabic text exceeds the space