One family, one documentation
Menu, MenuItem, MenuSubmenu, MenuGroup, MenuHeadline and ContextMenu form a single family. This page is their shared documentation; each component keeps a dedicated API reference:
All adapters use the same core contract, the same styles and the same keyboard navigation engine.
Choosing the right semantics
Use purpose="actions" (the default) for commands. The container then exposes menu and its items expose menuitem, menuitemradio or menuitemcheckbox.
Use purpose="selection" for a list of options. The container becomes a listbox, the items become options and their state is announced with aria-selected.
accessibleLabel names the menu when no external label is linked to it.
Composition
MenuItemrepresents a command or a choice.MenuSubmenurepresents a command that opens another actions menu.MenuGroupgroups items and itslabelnames the group.MenuHeadlineis a non-interactive visual marker. It does not replace the semantic label of aMenuGroup. In a menu made of groups, place it inside the relevantMenuGroupso it stays within its surface.ContextMenuopens an actions menu at the pointer position or viaShift+F10.
import { useState } from 'react';
import { Menu, MenuGroup, MenuHeadline, MenuItem } from '@udixio/ui-react';
export default function MenuFamilyReact() {
const [compact, setCompact] = useState(false);
return (
<Menu accessibleLabel="Editor actions">
<MenuGroup>
<MenuHeadline label="Document" />
<MenuItem label="Copy" />
<MenuItem label="Paste" disabled />
</MenuGroup>
<MenuGroup label="View">
<MenuItem
label="Compact mode"
selectionType="multiple"
selected={compact}
onSelectedChange={setCompact}
/>
</MenuGroup>
</Menu>
);
}import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Menu, MenuGroup, MenuHeadline, MenuItem } from '@udixio/ui-angular';
@Component({
selector: 'docs-menu-family-angular',
standalone: true,
imports: [Menu, MenuGroup, MenuHeadline, MenuItem],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<udx-menu accessibleLabel="Editor actions">
<udx-menu-group>
<udx-menu-headline label="Document" />
<udx-menu-item label="Copy" />
<udx-menu-item label="Paste" disabled />
</udx-menu-group>
<udx-menu-group label="View">
<udx-menu-item
label="Compact mode"
selectionType="multiple"
[selected]="compact()"
(selectedChange)="compact.set($event)"
/>
</udx-menu-group>
</udx-menu>
`,
})
export class MenuFamilyAngular {
protected readonly compact = signal(false);
}<script lang="ts">
import { Menu, MenuGroup, MenuHeadline, MenuItem } from '@udixio/ui-svelte';
let compact = $state(false);
</script>
<Menu accessibleLabel="Editor actions">
<MenuGroup>
<MenuHeadline label="Document" />
<MenuItem label="Copy" />
<MenuItem label="Paste" disabled />
</MenuGroup>
<MenuGroup label="View">
<MenuItem label="Compact mode" selectionType="multiple" bind:selected={compact} />
</MenuGroup>
</Menu>Variants, icons and states
variant="standard" uses the neutral surface. variant="vibrant" applies the family color to the whole composition. Leading and trailing icons remain optional; a disabled item keeps its structure but receives no interaction or state layer. In an actions menu, a leading icon on any command aligns the labels of the other action rows to the same icon column. Selection toggles keep their own checkmark column. MenuItem.trailingIcon stays decorative; it can mark a link such as “Open API reference” with an open-in-new icon. Use MenuSubmenu when the row should open another menu.
When there is no MenuGroup, a MenuHeadline can be placed directly inside Menu.
import { Menu, MenuHeadline, MenuItem } from '@udixio/ui-react';
import { iContentCopy } from '@udixio/icons-rounded-400/content_copy';
import { iOpenInNew } from '@udixio/icons-rounded-400/open_in_new';
export default function MenuVariantsReact() {
return (
<div className="flex flex-wrap items-start justify-center gap-6">
<Menu accessibleLabel="Standard actions">
<MenuHeadline label="Standard" />
<MenuItem label="Copy" leadingIcon={iContentCopy} />
<MenuItem
label="Open API reference"
href="/components/menu/api"
target="_blank"
trailingIcon={iOpenInNew}
/>
<MenuItem label="Delete" disabled />
</Menu>
<Menu accessibleLabel="Vibrant actions" variant="vibrant">
<MenuHeadline label="Vibrant" />
<MenuItem label="Copy" leadingIcon={iContentCopy} />
<MenuItem
label="Open API reference"
href="/components/menu/api"
target="_blank"
trailingIcon={iOpenInNew}
/>
<MenuItem label="Delete" disabled />
</Menu>
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Menu, MenuHeadline, MenuItem } from '@udixio/ui-angular';
import { iContentCopy } from '@udixio/icons-rounded-400/content_copy';
import { iOpenInNew } from '@udixio/icons-rounded-400/open_in_new';
@Component({
selector: 'docs-menu-variants-angular',
standalone: true,
imports: [Menu, MenuHeadline, MenuItem],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-wrap items-start justify-center gap-6">
<udx-menu accessibleLabel="Standard actions">
<udx-menu-headline label="Standard" />
<udx-menu-item label="Copy" [leadingIcon]="copyIcon" />
<udx-menu-item
label="Open API reference"
href="/components/menu/api"
target="_blank"
[trailingIcon]="openInNewIcon"
/>
<udx-menu-item label="Delete" disabled />
</udx-menu>
<udx-menu accessibleLabel="Vibrant actions" variant="vibrant">
<udx-menu-headline label="Vibrant" />
<udx-menu-item label="Copy" [leadingIcon]="copyIcon" />
<udx-menu-item
label="Open API reference"
href="/components/menu/api"
target="_blank"
[trailingIcon]="openInNewIcon"
/>
<udx-menu-item label="Delete" disabled />
</udx-menu>
</div>
`,
})
export class MenuVariantsAngular {
protected readonly copyIcon = iContentCopy;
protected readonly openInNewIcon = iOpenInNew;
}<script lang="ts">
import { iContentCopy } from '@udixio/icons-rounded-400/content_copy';
import { iOpenInNew } from '@udixio/icons-rounded-400/open_in_new';
import { Menu, MenuHeadline, MenuItem } from '@udixio/ui-svelte';
</script>
<div class="flex flex-wrap items-start justify-center gap-6">
<Menu accessibleLabel="Standard actions">
<MenuHeadline label="Standard" />
<MenuItem label="Copy" leadingIcon={iContentCopy} />
<MenuItem label="Open API reference" href="/components/menu/api" target="_blank" trailingIcon={iOpenInNew} />
<MenuItem label="Delete" disabled />
</Menu>
<Menu accessibleLabel="Vibrant actions" variant="vibrant">
<MenuHeadline label="Vibrant" />
<MenuItem label="Copy" leadingIcon={iContentCopy} />
<MenuItem label="Open API reference" href="/components/menu/api" target="_blank" trailingIcon={iOpenInNew} />
<MenuItem label="Delete" disabled />
</Menu>
</div>Nested action menus
Use MenuSubmenu for a row that opens another actions menu. It inherits the parent variant, opens on pointer hover or click, and supports Arrow Right to open and focus the first child (Arrow Left in RTL). The opposite arrow or Escape closes it and returns focus to the trigger. A child command closes its submenu; a checkbox toggle keeps it open.
import { Menu, MenuItem, MenuSubmenu } from '@udixio/ui-react';
import { iContentCopy } from '@udixio/icons-rounded-400/content_copy';
export default function MenuSubmenusReact() {
return (
<Menu accessibleLabel="Document actions">
<MenuItem label="Duplicate" leadingIcon={iContentCopy} />
<MenuItem label="Rename" />
<MenuSubmenu label="Share">
<MenuItem label="Copy link" />
<MenuItem label="Send by email" />
<MenuSubmenu label="More options">
<MenuItem label="Export" />
<MenuItem label="Publish" />
</MenuSubmenu>
</MenuSubmenu>
<MenuItem label="Move to trash" />
</Menu>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Menu, MenuItem, MenuSubmenu } from '@udixio/ui-angular';
import { iContentCopy } from '@udixio/icons-rounded-400/content_copy';
@Component({
selector: 'docs-menu-submenus-angular',
standalone: true,
imports: [Menu, MenuItem, MenuSubmenu],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<udx-menu accessibleLabel="Document actions">
<udx-menu-item label="Duplicate" [leadingIcon]="copyIcon" />
<udx-menu-item label="Rename" />
<udx-menu-submenu label="Share">
<udx-menu-item label="Copy link" />
<udx-menu-item label="Send by email" />
<udx-menu-submenu label="More options">
<udx-menu-item label="Export" />
<udx-menu-item label="Publish" />
</udx-menu-submenu>
</udx-menu-submenu>
<udx-menu-item label="Move to trash" />
</udx-menu>
`,
})
export class MenuSubmenusAngular {
protected readonly copyIcon = iContentCopy;
}<script lang="ts">
import { iContentCopy } from '@udixio/icons-rounded-400/content_copy';
import { Menu, MenuItem, MenuSubmenu } from '@udixio/ui-svelte';
</script>
<Menu accessibleLabel="Document actions">
<MenuItem label="Duplicate" leadingIcon={iContentCopy} />
<MenuItem label="Rename" />
<MenuSubmenu label="Share">
<MenuItem label="Copy link" />
<MenuItem label="Send by email" />
<MenuSubmenu label="More options">
<MenuItem label="Export" />
<MenuItem label="Publish" />
</MenuSubmenu>
</MenuSubmenu>
<MenuItem label="Move to trash" />
</Menu>Controlled and uncontrolled selection
A selectable item declares selectionType="single" or selectionType="multiple".
- In React, control it with
selectedandonSelectedChange. - In Angular, use
selectedandselectedChange, including with[(selected)]. - In Svelte, use
bind:selectedorselectedwithonSelectedChange. defaultSelectedinitializes an uncontrolled item.- An already selected
singleitem does not deselect itself; the group’s owner picks the new value.
The former names activated and onToggle were removed: they mixed resolved state with the event, and did not distinguish single from multiple selection.
The following example brings both models together:
- an actions menu with radio choices (
single) and checkboxes (multiple); - a list of options with
purpose="selection"; - controlled states and one state initialized with
defaultSelected.
import { useState } from 'react';
import { Menu, MenuGroup, MenuHeadline, MenuItem } from '@udixio/ui-react';
type Density = 'comfortable' | 'compact';
type Language = 'english' | 'french';
export default function MenuSelectionReact() {
const [density, setDensity] = useState<Density>('comfortable');
const [spellCheck, setSpellCheck] = useState(true);
const [language, setLanguage] = useState<Language>('english');
return (
<div className="flex flex-wrap items-start justify-center gap-6">
<Menu accessibleLabel="Editor preferences">
<MenuGroup label="Density">
<MenuItem
label="Comfortable"
selectionType="single"
selected={density === 'comfortable'}
onSelectedChange={(selected) =>
selected && setDensity('comfortable')
}
/>
<MenuItem
label="Compact"
selectionType="single"
selected={density === 'compact'}
onSelectedChange={(selected) => selected && setDensity('compact')}
/>
</MenuGroup>
<MenuGroup label="Tools">
<MenuItem
label="Spell checking"
selectionType="multiple"
selected={spellCheck}
onSelectedChange={setSpellCheck}
/>
<MenuItem
label="Grammar suggestions"
selectionType="multiple"
defaultSelected
/>
</MenuGroup>
</Menu>
<Menu purpose="selection" accessibleLabel="Language">
<MenuHeadline label="Language" />
<MenuItem
label="English"
selected={language === 'english'}
onSelectedChange={(selected) => selected && setLanguage('english')}
/>
<MenuItem
label="French"
selected={language === 'french'}
onSelectedChange={(selected) => selected && setLanguage('french')}
/>
</Menu>
</div>
);
}import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Menu, MenuGroup, MenuHeadline, MenuItem } from '@udixio/ui-angular';
type Density = 'comfortable' | 'compact';
type Language = 'english' | 'french';
@Component({
selector: 'docs-menu-selection-angular',
standalone: true,
imports: [Menu, MenuGroup, MenuHeadline, MenuItem],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-wrap items-start justify-center gap-6">
<udx-menu accessibleLabel="Editor preferences">
<udx-menu-group label="Density">
<udx-menu-item
label="Comfortable"
selectionType="single"
[selected]="density() === 'comfortable'"
(selectedChange)="selectDensity('comfortable', $event)"
/>
<udx-menu-item
label="Compact"
selectionType="single"
[selected]="density() === 'compact'"
(selectedChange)="selectDensity('compact', $event)"
/>
</udx-menu-group>
<udx-menu-group label="Tools">
<udx-menu-item
label="Spell checking"
selectionType="multiple"
[selected]="spellCheck()"
(selectedChange)="spellCheck.set($event)"
/>
<udx-menu-item
label="Grammar suggestions"
selectionType="multiple"
defaultSelected
/>
</udx-menu-group>
</udx-menu>
<udx-menu purpose="selection" accessibleLabel="Language">
<udx-menu-headline label="Language" />
<udx-menu-item
label="English"
[selected]="language() === 'english'"
(selectedChange)="selectLanguage('english', $event)"
/>
<udx-menu-item
label="French"
[selected]="language() === 'french'"
(selectedChange)="selectLanguage('french', $event)"
/>
</udx-menu>
</div>
`,
})
export class MenuSelectionAngular {
protected readonly density = signal<Density>('comfortable');
protected readonly spellCheck = signal(true);
protected readonly language = signal<Language>('english');
protected selectDensity(value: Density, selected: boolean): void {
if (selected) this.density.set(value);
}
protected selectLanguage(value: Language, selected: boolean): void {
if (selected) this.language.set(value);
}
}<script lang="ts">
import { Menu, MenuGroup, MenuHeadline, MenuItem } from '@udixio/ui-svelte';
type Density = 'comfortable' | 'compact';
type Language = 'english' | 'french';
let density = $state<Density>('comfortable');
let spellCheck = $state(true);
let language = $state<Language>('english');
</script>
<div class="flex flex-wrap items-start justify-center gap-6">
<Menu accessibleLabel="Editor preferences">
<MenuGroup label="Density">
<MenuItem
label="Comfortable"
selectionType="single"
selected={density === 'comfortable'}
onSelectedChange={(selected) => selected && (density = 'comfortable')}
/>
<MenuItem
label="Compact"
selectionType="single"
selected={density === 'compact'}
onSelectedChange={(selected) => selected && (density = 'compact')}
/>
</MenuGroup>
<MenuGroup label="Tools">
<MenuItem label="Spell checking" selectionType="multiple" bind:selected={spellCheck} />
<MenuItem label="Grammar suggestions" selectionType="multiple" defaultSelected />
</MenuGroup>
</Menu>
<Menu purpose="selection" accessibleLabel="Language">
<MenuHeadline label="Language" />
<MenuItem
label="English"
selected={language === 'english'}
onSelectedChange={(selected) => selected && (language = 'english')}
/>
<MenuItem
label="French"
selected={language === 'french'}
onSelectedChange={(selected) => selected && (language = 'french')}
/>
</Menu>
</div>Links and scrollable content
With href, a MenuItem becomes a native link. A disabled link loses its destination and leaves the tab order. When the content exceeds the menu’s maximum height, the surface becomes scrollable without changing keyboard navigation.
import { Menu, MenuHeadline, MenuItem } from '@udixio/ui-react';
const recentDocuments = Array.from(
{ length: 10 },
(_, index) => `Document ${index + 1}`,
);
export default function MenuLinksScrollReact() {
return (
<div className="flex flex-wrap items-start justify-center gap-6">
<Menu accessibleLabel="Component documentation">
<MenuHeadline label="Documentation" />
<MenuItem label="Button" href="/components/button/overview" />
<MenuItem label="Card" href="/components/card/overview" />
<MenuItem label="Checkbox" href="/components/checkbox/overview" />
<MenuItem label="Unavailable page" href="/unavailable" disabled />
</Menu>
<Menu accessibleLabel="Recent documents">
<MenuHeadline label="Recent documents" />
{recentDocuments.map((document) => (
<MenuItem key={document} label={document} />
))}
</Menu>
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Menu, MenuHeadline, MenuItem } from '@udixio/ui-angular';
@Component({
selector: 'docs-menu-links-scroll-angular',
standalone: true,
imports: [Menu, MenuHeadline, MenuItem],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-wrap items-start justify-center gap-6">
<udx-menu accessibleLabel="Component documentation">
<udx-menu-headline label="Documentation" />
<udx-menu-item label="Button" href="/components/button/overview" />
<udx-menu-item label="Card" href="/components/card/overview" />
<udx-menu-item label="Checkbox" href="/components/checkbox/overview" />
<udx-menu-item label="Unavailable page" href="/unavailable" disabled />
</udx-menu>
<udx-menu accessibleLabel="Recent documents">
<udx-menu-headline label="Recent documents" />
@for (document of recentDocuments; track document) {
<udx-menu-item [label]="document" />
}
</udx-menu>
</div>
`,
})
export class MenuLinksScrollAngular {
protected readonly recentDocuments = Array.from(
{ length: 10 },
(_, index) => `Document ${index + 1}`,
);
}<script lang="ts">
import { Menu, MenuHeadline, MenuItem } from '@udixio/ui-svelte';
const recentDocuments = Array.from({ length: 10 }, (_, index) => `Document ${index + 1}`);
</script>
<div class="flex flex-wrap items-start justify-center gap-6">
<Menu accessibleLabel="Component documentation">
<MenuHeadline label="Documentation" />
<MenuItem label="Button" href="/components/button/overview" />
<MenuItem label="Card" href="/components/card/overview" />
<MenuItem label="Checkbox" href="/components/checkbox/overview" />
<MenuItem label="Unavailable page" href="/unavailable" disabled />
</Menu>
<Menu accessibleLabel="Recent documents">
<MenuHeadline label="Recent documents" />
{#each recentDocuments as recentDocument (recentDocument)}
<MenuItem label={recentDocument} />
{/each}
</Menu>
</div>Menu exposed in a select field
TextField with type="select" opens a purpose="selection" surface, controls the selected value and closes the menu after the choice. Both adapters share this behavior; only the way options are declared differs:
- in React, compose
MenuItemchildren directly, or passoptions; - in Angular,
optionsis the only way to populate the menu: the adapter does not project customMenuItems. - in Svelte,
optionsis also the select-field model; customMenuItemchildren are not projected by this mode.
import { useState } from 'react';
import { MenuItem, TextField } from '@udixio/ui-react';
export default function MenuSelectFieldReact() {
const [fruit, setFruit] = useState('apple');
return (
<div className="w-full max-w-sm">
<TextField
type="select"
label="Favorite fruit"
value={fruit}
onChange={setFruit}
>
<MenuItem label="Apple" value="apple" />
<MenuItem label="Banana" value="banana" />
<MenuItem label="Orange" value="orange" />
</TextField>
</div>
);
}import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { TextField } from '@udixio/ui-angular';
import type { TextFieldOption } from '@udixio/core';
@Component({
selector: 'docs-menu-select-field-angular',
standalone: true,
imports: [TextField],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="w-full max-w-sm">
<udx-text-field
label="Favorite fruit"
name="fruit"
type="select"
[options]="options"
[(value)]="fruit"
/>
</div>
`,
})
export class MenuSelectFieldAngular {
protected readonly fruit = signal('apple');
protected readonly options: TextFieldOption[] = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'orange', label: 'Orange' },
];
}<script lang="ts">
import { TextField } from '@udixio/ui-svelte';
const options = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'orange', label: 'Orange' },
];
let fruit = $state('apple');
</script>
<div class="w-full max-w-sm">
<TextField type="select" label="Favorite fruit" bind:value={fruit} {options} />
</div>Context menu
The context menu opens on secondary click or with Shift+F10. It moves focus to the first available item. Escape closes the popup and restores focus to the trigger; an outside click or a scroll also closes it.
import {
ContextMenu,
MenuGroup,
MenuHeadline,
MenuItem,
} from '@udixio/ui-react';
const scrollableActions = [
'Rename',
'Duplicate',
'Move to folder',
'Make available offline',
'Add to favorites',
'Share',
'Download',
'Archive',
'Delete',
];
export default function ContextMenuReact() {
return (
<div className="flex flex-wrap gap-3">
<ContextMenu
accessibleLabel="Simple document actions"
trigger={
<button
type="button"
className="rounded-xl border border-outline px-4 py-3"
>
Simple actions
</button>
}
>
<MenuItem label="Rename" />
<MenuItem label="Duplicate" />
<MenuItem label="Delete" />
</ContextMenu>
<ContextMenu
accessibleLabel="Grouped document actions"
trigger={
<button
type="button"
className="rounded-xl border border-outline px-4 py-3"
>
Grouped actions
</button>
}
>
<MenuGroup>
<MenuHeadline label="Document" />
<MenuItem label="Rename" />
<MenuItem label="Duplicate" />
</MenuGroup>
<MenuGroup label="View">
<MenuItem label="Open preview" />
<MenuItem label="Show details" />
</MenuGroup>
</ContextMenu>
<ContextMenu
accessibleLabel="Many document actions"
trigger={
<button
type="button"
className="rounded-xl border border-outline px-4 py-3"
>
Long actions
</button>
}
>
{scrollableActions.map((label) => (
<MenuItem key={label} label={label} />
))}
</ContextMenu>
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
ContextMenu,
MenuGroup,
MenuHeadline,
MenuItem,
} from '@udixio/ui-angular';
@Component({
selector: 'docs-context-menu-angular',
standalone: true,
imports: [ContextMenu, MenuGroup, MenuHeadline, MenuItem],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-wrap gap-3">
<udx-context-menu accessibleLabel="Simple document actions">
<button
contextMenuTrigger
type="button"
class="rounded-xl border border-outline px-4 py-3"
>
Simple actions
</button>
<udx-menu-item label="Rename" />
<udx-menu-item label="Duplicate" />
<udx-menu-item label="Delete" />
</udx-context-menu>
<udx-context-menu accessibleLabel="Grouped document actions">
<button
contextMenuTrigger
type="button"
class="rounded-xl border border-outline px-4 py-3"
>
Grouped actions
</button>
<udx-menu-group>
<udx-menu-headline label="Document" />
<udx-menu-item label="Rename" />
<udx-menu-item label="Duplicate" />
</udx-menu-group>
<udx-menu-group label="View">
<udx-menu-item label="Open preview" />
<udx-menu-item label="Show details" />
</udx-menu-group>
</udx-context-menu>
<udx-context-menu accessibleLabel="Many document actions">
<button
contextMenuTrigger
type="button"
class="rounded-xl border border-outline px-4 py-3"
>
Long actions
</button>
@for (label of scrollableActions; track label) {
<udx-menu-item [label]="label" />
}
</udx-context-menu>
</div>
`,
})
export class ContextMenuAngular {
protected readonly scrollableActions = [
'Rename',
'Duplicate',
'Move to folder',
'Make available offline',
'Add to favorites',
'Share',
'Download',
'Archive',
'Delete',
];
}<script lang="ts">
import {
ContextMenu,
MenuGroup,
MenuHeadline,
MenuItem,
} from '@udixio/ui-svelte';
const scrollableActions = [
'Rename',
'Duplicate',
'Move to folder',
'Make available offline',
'Add to favorites',
'Share',
'Download',
'Archive',
'Delete',
];
</script>
<div class="flex flex-wrap gap-3">
<ContextMenu accessibleLabel="Simple document actions">
{#snippet trigger()}
<button type="button" class="rounded-xl border border-outline px-4 py-3">
Simple actions
</button>
{/snippet}
<MenuItem label="Rename" />
<MenuItem label="Duplicate" />
<MenuItem label="Delete" />
</ContextMenu>
<ContextMenu accessibleLabel="Grouped document actions">
{#snippet trigger()}
<button type="button" class="rounded-xl border border-outline px-4 py-3">
Grouped actions
</button>
{/snippet}
<MenuGroup>
<MenuHeadline label="Document" />
<MenuItem label="Rename" />
<MenuItem label="Duplicate" />
</MenuGroup>
<MenuGroup label="View">
<MenuItem label="Open preview" />
<MenuItem label="Show details" />
</MenuGroup>
</ContextMenu>
<ContextMenu accessibleLabel="Many document actions">
{#snippet trigger()}
<button type="button" class="rounded-xl border border-outline px-4 py-3">
Long actions
</button>
{/snippet}
{#each scrollableActions as label}
<MenuItem {label} />
{/each}
</ContextMenu>
</div>Keyboard navigation
In an open menu:
- Arrow down/up moves focus and wraps at both ends.
- Arrow Right opens a focused submenu trigger and Arrow Left returns to its parent (the directions reverse in RTL).
- Home/End goes to the first/last available item.
- Typing a character searches for the next matching label.
- Disabled items are skipped.