0
# Submenus
1
2
Components for creating nested submenus and additional visual elements like arrows, enabling multi-level menu hierarchies.
3
4
## Capabilities
5
6
### MenubarSub (Sub)
7
8
Container for submenu functionality with controllable open state, managing the visibility and behavior of nested menu content.
9
10
```typescript { .api }
11
/**
12
* Container for submenu with controllable open state
13
* @param props - Submenu container props
14
* @returns JSX element representing the submenu container
15
*/
16
function MenubarSub(props: MenubarSubProps): React.ReactElement;
17
18
interface MenubarSubProps {
19
/** Child components (typically SubTrigger and SubContent) */
20
children?: React.ReactNode;
21
/** Controlled open state */
22
open?: boolean;
23
/** Default open state for uncontrolled usage (default: false) */
24
defaultOpen?: boolean;
25
/** Callback fired when open state changes */
26
onOpenChange?: (open: boolean) => void;
27
}
28
```
29
30
**Usage Examples:**
31
32
```typescript
33
'use client';
34
import * as Menubar from "@radix-ui/react-menubar";
35
36
// Basic submenu
37
<Menubar.Sub>
38
<Menubar.SubTrigger>
39
Export
40
<ChevronRightIcon />
41
</Menubar.SubTrigger>
42
<Menubar.Portal>
43
<Menubar.SubContent>
44
<Menubar.Item>Export as PDF</Menubar.Item>
45
<Menubar.Item>Export as Image</Menubar.Item>
46
</Menubar.SubContent>
47
</Menubar.Portal>
48
</Menubar.Sub>
49
50
// Controlled submenu
51
function ControlledSubmenu() {
52
const [isOpen, setIsOpen] = React.useState(false);
53
54
return (
55
<Menubar.Sub open={isOpen} onOpenChange={setIsOpen}>
56
<Menubar.SubTrigger>
57
Tools {isOpen ? "▼" : "▶"}
58
</Menubar.SubTrigger>
59
<Menubar.Portal>
60
<Menubar.SubContent>
61
<Menubar.Item>Format Document</Menubar.Item>
62
<Menubar.Item>Check Spelling</Menubar.Item>
63
</Menubar.SubContent>
64
</Menubar.Portal>
65
</Menubar.Sub>
66
);
67
}
68
```
69
70
### MenubarSubTrigger (SubTrigger)
71
72
Trigger that opens a submenu when hovered or activated, typically displaying an arrow indicator.
73
74
```typescript { .api }
75
/**
76
* Trigger that opens a submenu on hover or activation
77
* @param props - Sub-trigger props
78
* @returns JSX element representing the submenu trigger
79
*/
80
function MenubarSubTrigger(props: MenubarSubTriggerProps): React.ReactElement;
81
82
interface MenubarSubTriggerProps extends React.ComponentPropsWithoutRef<'div'> {
83
/** Whether the trigger is disabled */
84
disabled?: boolean;
85
/** Text value for accessibility */
86
textValue?: string;
87
}
88
```
89
90
**Usage Examples:**
91
92
```typescript
93
// Basic sub-trigger with arrow
94
<Menubar.SubTrigger>
95
Recent Files
96
<ChevronRightIcon className="sub-arrow" />
97
</Menubar.SubTrigger>
98
99
// Sub-trigger with custom content
100
<Menubar.SubTrigger>
101
<FileIcon />
102
<span>Export Options</span>
103
<ArrowIcon />
104
</Menubar.SubTrigger>
105
106
// Disabled sub-trigger
107
<Menubar.SubTrigger disabled>
108
Advanced Tools
109
<ChevronRightIcon />
110
</Menubar.SubTrigger>
111
```
112
113
### MenubarSubContent (SubContent)
114
115
Content container for submenu items, positioned relative to the sub-trigger.
116
117
```typescript { .api }
118
/**
119
* Content container for submenu items
120
* @param props - Sub-content props
121
* @returns JSX element representing the submenu content
122
*/
123
function MenubarSubContent(props: MenubarSubContentProps): React.ReactElement;
124
125
interface MenubarSubContentProps extends React.ComponentPropsWithoutRef<'div'> {
126
/** Alignment relative to trigger */
127
align?: 'start' | 'center' | 'end';
128
/** Side preference for positioning */
129
side?: 'top' | 'right' | 'bottom' | 'left';
130
/** Distance from trigger in pixels */
131
sideOffset?: number;
132
/** Alignment offset in pixels */
133
alignOffset?: number;
134
/** Whether content should avoid collisions with the boundary */
135
avoidCollisions?: boolean;
136
/** Element or area to constrain positioning within */
137
collisionBoundary?: Element | null | Array<Element | null>;
138
/** Padding from collision boundary in pixels */
139
collisionPadding?: number | Partial<Record<'top' | 'right' | 'bottom' | 'left', number>>;
140
/** Whether content should stick to trigger when boundary is reached */
141
sticky?: 'partial' | 'always';
142
/** Callback fired when content loses focus */
143
onCloseAutoFocus?: (event: Event) => void;
144
/** Callback fired when escape key is pressed */
145
onEscapeKeyDown?: (event: KeyboardEvent) => void;
146
/** Callback fired when pointer moves outside */
147
onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;
148
/** Callback fired when focus moves outside the content */
149
onFocusOutside?: (event: FocusOutsideEvent) => void;
150
/** Callback fired when interaction occurs outside the content */
151
onInteractOutside?: (event: InteractOutsideEvent) => void;
152
}
153
```
154
155
**Usage Examples:**
156
157
```typescript
158
// Basic sub-content
159
<Menubar.SubContent>
160
<Menubar.Item>Export as PDF</Menubar.Item>
161
<Menubar.Item>Export as PNG</Menubar.Item>
162
<Menubar.Item>Export as SVG</Menubar.Item>
163
</Menubar.SubContent>
164
165
// Sub-content with custom positioning
166
<Menubar.SubContent
167
side="right"
168
align="start"
169
sideOffset={2}
170
>
171
<Menubar.Item>Option 1</Menubar.Item>
172
<Menubar.Item>Option 2</Menubar.Item>
173
</Menubar.SubContent>
174
175
// Sub-content with nested submenus
176
<Menubar.SubContent>
177
<Menubar.Item>Quick Export</Menubar.Item>
178
179
<Menubar.Sub>
180
<Menubar.SubTrigger>
181
Advanced Export
182
<ChevronRightIcon />
183
</Menubar.SubTrigger>
184
<Menubar.Portal>
185
<Menubar.SubContent>
186
<Menubar.Item>With Metadata</Menubar.Item>
187
<Menubar.Item>Compressed</Menubar.Item>
188
</Menubar.SubContent>
189
</Menubar.Portal>
190
</Menubar.Sub>
191
</Menubar.SubContent>
192
```
193
194
**Key Implementation Details:**
195
196
- **Data Attributes**:
197
- SubTrigger automatically receives `data-radix-menubar-subtrigger=""` attribute
198
- SubContent automatically receives `data-radix-menubar-content=""` attribute
199
- **CSS Custom Properties**: SubContent exposes the same positioning variables as MenubarContent:
200
- `--radix-menubar-content-transform-origin`: Transform origin for animations
201
- `--radix-menubar-content-available-width`: Available width for content
202
- `--radix-menubar-content-available-height`: Available height for content
203
- `--radix-menubar-trigger-width`: Width of the triggering element
204
- `--radix-menubar-trigger-height`: Height of the triggering element
205
- **Navigation Behavior**: Arrow keys navigate horizontally between main menu triggers, preventing submenu opening
206
207
### MenubarArrow (Arrow)
208
209
Optional arrow pointing from trigger to content, providing visual connection between trigger and dropdown.
210
211
```typescript { .api }
212
/**
213
* Optional arrow pointing from trigger to content
214
* @param props - Arrow props
215
* @returns JSX element representing the arrow
216
*/
217
function MenubarArrow(props: MenubarArrowProps): React.ReactElement;
218
219
interface MenubarArrowProps extends React.ComponentPropsWithoutRef<'svg'> {
220
/** Width of the arrow in pixels (default: 10) */
221
width?: number;
222
/** Height of the arrow in pixels (default: 5) */
223
height?: number;
224
}
225
```
226
227
**Usage Examples:**
228
229
```typescript
230
// Basic arrow
231
<Menubar.Portal>
232
<Menubar.Content>
233
<Menubar.Arrow />
234
<Menubar.Item>Menu Item</Menubar.Item>
235
</Menubar.Content>
236
</Menubar.Portal>
237
238
// Custom sized arrow
239
<Menubar.Portal>
240
<Menubar.Content>
241
<Menubar.Arrow width={12} height={6} />
242
<Menubar.Item>Menu Item</Menubar.Item>
243
</Menubar.Content>
244
</Menubar.Portal>
245
246
// Styled arrow
247
<Menubar.Portal>
248
<Menubar.Content>
249
<Menubar.Arrow className="custom-arrow" />
250
<Menubar.Item>Menu Item</Menubar.Item>
251
</Menubar.Content>
252
</Menubar.Portal>
253
```
254
255
## Complex Submenu Examples
256
257
### Multi-Level Menu Structure
258
259
```typescript
260
function MultiLevelMenu() {
261
return (
262
<Menubar.Content>
263
<Menubar.Item>New File</Menubar.Item>
264
<Menubar.Item>Open</Menubar.Item>
265
266
<Menubar.Sub>
267
<Menubar.SubTrigger>
268
Recent Files
269
<ChevronRightIcon />
270
</Menubar.SubTrigger>
271
<Menubar.Portal>
272
<Menubar.SubContent>
273
<Menubar.Item>document1.txt</Menubar.Item>
274
<Menubar.Item>project.json</Menubar.Item>
275
276
<Menubar.Sub>
277
<Menubar.SubTrigger>
278
More Files
279
<ChevronRightIcon />
280
</Menubar.SubTrigger>
281
<Menubar.Portal>
282
<Menubar.SubContent>
283
<Menubar.Item>old-project.js</Menubar.Item>
284
<Menubar.Item>backup.sql</Menubar.Item>
285
</Menubar.SubContent>
286
</Menubar.Portal>
287
</Menubar.Sub>
288
</Menubar.SubContent>
289
</Menubar.Portal>
290
</Menubar.Sub>
291
292
<Menubar.Separator />
293
294
<Menubar.Sub>
295
<Menubar.SubTrigger>
296
Export
297
<ChevronRightIcon />
298
</Menubar.SubTrigger>
299
<Menubar.Portal>
300
<Menubar.SubContent>
301
<Menubar.Item>Export as PDF</Menubar.Item>
302
<Menubar.Item>Export as HTML</Menubar.Item>
303
304
<Menubar.Sub>
305
<Menubar.SubTrigger>
306
Export as Image
307
<ChevronRightIcon />
308
</Menubar.SubTrigger>
309
<Menubar.Portal>
310
<Menubar.SubContent>
311
<Menubar.Item>PNG</Menubar.Item>
312
<Menubar.Item>JPEG</Menubar.Item>
313
<Menubar.Item>SVG</Menubar.Item>
314
</Menubar.SubContent>
315
</Menubar.Portal>
316
</Menubar.Sub>
317
</Menubar.SubContent>
318
</Menubar.Portal>
319
</Menubar.Sub>
320
</Menubar.Content>
321
);
322
}
323
```
324
325
### Dynamic Submenu Content
326
327
```typescript
328
function DynamicSubmenu() {
329
const [recentFiles, setRecentFiles] = React.useState([
330
"document1.txt",
331
"project.json",
332
"notes.md"
333
]);
334
335
return (
336
<Menubar.Sub>
337
<Menubar.SubTrigger>
338
Recent Files ({recentFiles.length})
339
<ChevronRightIcon />
340
</Menubar.SubTrigger>
341
<Menubar.Portal>
342
<Menubar.SubContent>
343
{recentFiles.length > 0 ? (
344
recentFiles.map((file, index) => (
345
<Menubar.Item
346
key={file}
347
onSelect={() => openFile(file)}
348
>
349
{file}
350
</Menubar.Item>
351
))
352
) : (
353
<Menubar.Label>No recent files</Menubar.Label>
354
)}
355
356
{recentFiles.length > 0 && (
357
<>
358
<Menubar.Separator />
359
<Menubar.Item onSelect={() => setRecentFiles([])}>
360
Clear Recent Files
361
</Menubar.Item>
362
</>
363
)}
364
</Menubar.SubContent>
365
</Menubar.Portal>
366
</Menubar.Sub>
367
);
368
}
369
```
370
371
## Type Definitions
372
373
```typescript { .api }
374
// Element reference types
375
type MenubarSubTriggerElement = HTMLDivElement;
376
type MenubarSubContentElement = HTMLDivElement;
377
type MenubarArrowElement = SVGSVGElement;
378
379
// Event types
380
interface PointerDownOutsideEvent {
381
target: HTMLElement;
382
preventDefault(): void;
383
}
384
385
interface FocusOutsideEvent {
386
target: HTMLElement;
387
preventDefault(): void;
388
}
389
390
interface InteractOutsideEvent {
391
target: HTMLElement;
392
preventDefault(): void;
393
}
394
```