One family, one documentation
Menu, MenuItem, MenuGroup, MenuHeadline and ContextMenu form a single family. This page is their shared documentation; each component keeps a dedicated API reference:
Both 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.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.
When there is no MenuGroup, a MenuHeadline can be placed directly inside Menu.
import { Menu, MenuHeadline, MenuItem } from '@udixio/ui-react';
import { iChevronRight } from '@udixio/icons-rounded-400/chevron_right';
import { iContentCopy } from '@udixio/icons-rounded-400/content_copy';
import { iDelete } from '@udixio/icons-rounded-400/delete';
import { iSettings } from '@udixio/icons-rounded-400/settings';
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="Settings"
leadingIcon={iSettings}
trailingIcon={iChevronRight}
/>
<MenuItem label="Delete" leadingIcon={iDelete} disabled />
</Menu>
<Menu accessibleLabel="Vibrant actions" variant="vibrant">
<MenuHeadline label="Vibrant" />
<MenuItem label="Copy" leadingIcon={iContentCopy} />
<MenuItem
label="Settings"
leadingIcon={iSettings}
trailingIcon={iChevronRight}
/>
<MenuItem label="Delete" leadingIcon={iDelete} disabled />
</Menu>
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Menu, MenuHeadline, MenuItem } from '@udixio/ui-angular';
import { iChevronRight } from '@udixio/icons-rounded-400/chevron_right';
import { iContentCopy } from '@udixio/icons-rounded-400/content_copy';
import { iDelete } from '@udixio/icons-rounded-400/delete';
import { iSettings } from '@udixio/icons-rounded-400/settings';
@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="Settings"
[leadingIcon]="settingsIcon"
[trailingIcon]="chevronRightIcon"
/>
<udx-menu-item label="Delete" [leadingIcon]="deleteIcon" 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="Settings"
[leadingIcon]="settingsIcon"
[trailingIcon]="chevronRightIcon"
/>
<udx-menu-item label="Delete" [leadingIcon]="deleteIcon" disabled />
</udx-menu>
</div>
`,
})
export class MenuVariantsAngular {
protected readonly chevronRightIcon = iChevronRight;
protected readonly copyIcon = iContentCopy;
protected readonly deleteIcon = iDelete;
protected readonly settingsIcon = iSettings;
}<script lang="ts">
import { iChevronRight } from '@udixio/icons-rounded-400/chevron_right';
import { iContentCopy } from '@udixio/icons-rounded-400/content_copy';
import { iDelete } from '@udixio/icons-rounded-400/delete';
import { iSettings } from '@udixio/icons-rounded-400/settings';
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="Settings" leadingIcon={iSettings} trailingIcon={iChevronRight} />
<MenuItem label="Delete" leadingIcon={iDelete} disabled />
</Menu>
<Menu accessibleLabel="Vibrant actions" variant="vibrant">
<MenuHeadline label="Vibrant" />
<MenuItem label="Copy" leadingIcon={iContentCopy} />
<MenuItem label="Settings" leadingIcon={iSettings} trailingIcon={iChevronRight} />
<MenuItem label="Delete" leadingIcon={iDelete} disabled />
</Menu>
</div>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, MenuItem } from '@udixio/ui-react';
export default function ContextMenuReact() {
return (
<ContextMenu
accessibleLabel="Document actions"
trigger={
<button className="rounded-xl border border-outline px-6 py-4">
Right-click or press Shift+F10
</button>
}
>
<MenuItem label="Rename" />
<MenuItem label="Duplicate" />
<MenuItem label="Delete" />
</ContextMenu>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ContextMenu, MenuItem } from '@udixio/ui-angular';
@Component({
selector: 'docs-context-menu-angular',
standalone: true,
imports: [ContextMenu, MenuItem],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<udx-context-menu accessibleLabel="Document actions">
<button
contextMenuTrigger
class="rounded-xl border border-outline px-6 py-4"
>
Right-click or press Shift+F10
</button>
<udx-menu-item label="Rename" />
<udx-menu-item label="Duplicate" />
<udx-menu-item label="Delete" />
</udx-context-menu>
`,
})
export class ContextMenuAngular {}<script lang="ts">
import { ContextMenu, MenuItem } from '@udixio/ui-svelte';
</script>
<ContextMenu accessibleLabel="Document actions">
{#snippet trigger()}
<button type="button" class="rounded-xl border border-outline px-6 py-4">
Right-click or press Shift+F10
</button>
{/snippet}
<MenuItem label="Rename" />
<MenuItem label="Duplicate" />
<MenuItem label="Delete" />
</ContextMenu>Keyboard navigation
In an open menu:
- Arrow down/up moves focus and wraps at both ends.
- Home/End goes to the first/last available item.
- Typing a character searches for the next matching label.
- Disabled items are skipped.