Skip to main content
In progress

Menu

Menu is a low-level component that renders a positioned dropdown menu list with keyboard navigation, focus management, and automatic positioning. It provides the foundation for menu-based interactions in YDS.

Code example
import { useEffect, useRef, useState } from 'react';
import {
Menu as YDSMenu,
MenuContext,
Button,
MenuItem,
MenuSeparator,
} from '@yleisradio/yds-components-react';
import { ChevronDown, Edit, Share, Trash } from '@yleisradio/yds-icons-react';

export const Menu = () => {
const menuButtonRef = useRef(null);
const [menuRoot, setMenuRoot] = useState<HTMLElement | null>(null);

useEffect(() => {
setMenuRoot(menuButtonRef.current);
}, []);

return (
<>
<Button
ref={menuButtonRef}
variant="text"
removePadding
iconAfter={<ChevronDown />}
onClick={() => {
if (menuRoot) {
setMenuRoot(null);
} else {
setMenuRoot(menuButtonRef.current);
}
}}
>
Menu
</Button>
<MenuContext.Provider value={{ menuSize: 'md' }}>
<YDSMenu id="menu" rootElement={menuRoot} buttonRef={menuButtonRef}>
<MenuItem icon={<Share />}>Share</MenuItem>
<MenuItem icon={<Edit />}>Edit</MenuItem>
<MenuSeparator />
<MenuItem icon={<Trash />}>Delete</MenuItem>
</YDSMenu>
</MenuContext.Provider>
</>
);
};

Why to use

Menu provides a shared, internal foundation for all dropdown-style components in the design system. It centralizes complex logic for menu components in a consistent way.

By building ActionMenu, DropdownMenu, and other menu-based components on top of Menu, the design system ensures predictable behavior, accessibility, and visual consistency across products without duplicating low-level logic.

When to use

Menu provides the underlying functionality for dropdown menus in YDS. Rely on Menu directly when you build menu-based patterns inside YDS or use higher-level components like ActionMenu or DropdownMenu that are built on top of Menu and provide specific interaction patterns.

Do
  • Use Menu as the foundation for building custom menu-based components or patterns with YDS.
Don't
  • Don't hide primary calls to action inside menus. Use menus for grouping secondary or contextual actions.
  • Don't use Menu for navigation. Use ButtonGroup or NavigationTabs instead.
  • Don't use multiple menu variants in the same space (e.g. mixing radio and checkbox menus) - use DropdownMenuGroup instead.
ComponentPurposeWhen to use
ActionMenuDisplays a list of actions without persistent selection.When the user needs to trigger one of several related actions (e.g. edit, delete, share).
DropdownMenuDisplays selectable options with single or multiple selection.When the user needs to choose values from a list (e.g. filters, sorting).
DropdownMenuGroupGroups multiple dropdown menus into a unified control area.When several related selections belong to the same task or context.
Other menu-based componentsReuse Menu for consistent dropdown behavior.When building new YDS components that require menu-like interactions.

Content guidelines

Do
  • Keep labels short, scannable, and consistent in structure.
  • Use verbs for actions (e.g. “Muokkaa”, “Jaa”, “Poista”) and nouns/adjectives for options (e.g. “Uusin”, “Suomi”).
  • Use MenuSeparator to group related items when the list has clear sections..
Don't
  • Don’t use long sentences, instructions, or mixed grammar styles in the same menu.
  • Don’t use vague labels like “OK”, “Kyllä”, or “Valitse” without context.
  • Don’t create deep or nested menu structures — split into multiple menus or rethink the model.

Anatomy

Menu anatomy

  1. Menu (surface) – The floating container rendered by the Menu component, positioned relative to the trigger (rootElement).
  2. MenuItem – An individual interactive row inside the menu, representing an action or selectable option.
  3. MenuItem label – The primary text content of a MenuItem, describing the action or option.
  4. MenuItem icon (optional) – An optional leading icon inside a MenuItem, used to support recognition or categorization.
  5. Selected state – Visual indication applied to a MenuItem when it represents the current value in selection-based menus.
  6. MenuSeparator (optional) – A visual divider rendered with MenuSeparator to group related menu items.

NOTE! Selection groups (e.g. radio or checkbox) can be rendered inside the menu to support single-select or multi-select patterns in structured menus.

Key MenuItem props

Use the following props to configure the MenuItem component.

icon

Optional icon element displayed before the menu item label.

TypeExampleDescription
ReactElement | nullIcon component to display before the label
Code example
import { MenuItem } from '@yleisradio/yds-components-react';
import { Share } from '@yleisradio/yds-icons-react';

<MenuItem icon={<Share />}>Share</MenuItem>

iconHighlight

Whether to highlight the icon area when the item is selected. Used to create space for a checkmark icon when isSelected is true.

TypeExampleDescription
booleanHighlights icon area for selected state
Code example
<MenuItem
iconHighlight
isSelected
>
Selected option
</MenuItem>

isDisabled

Whether the menu item is disabled and non-interactive.

TypeExampleDescription
booleanDisables the menu item
Code example
<MenuItem isDisabled>
Disabled option
</MenuItem>

isSelected

Whether the menu item is in a selected state. Used for selection-based menus (e.g., DropdownMenu).

TypeExampleDescription
booleanIndicates selected state
Code example
<MenuItem isSelected>
Selected option
</MenuItem>

itemVariant

Visual variant of the menu item. Determines how the item is rendered.

ValueExampleDescription
defaultDefault variant with icon and label
radioRadio button style for single selection
checkboxCheckbox style for multiple selection
Code example
<MenuItem itemVariant="default" icon={<Share />}>
Default item
</MenuItem>
<MenuItem itemVariant="radio" isSelected>
Radio item
</MenuItem>
<MenuItem itemVariant="checkbox" isSelected>
Checkbox item
</MenuItem>

children

The label text or content displayed in the menu item.

TypeExampleDescription
React.ReactNodeContent to display in the menu item
Code example
<MenuItem onSelect={() => {}}>
Simple text label
</MenuItem>
<MenuItem onSelect={() => {}}>
<span>Custom content</span>
</MenuItem>

Behavior

  • Menu is rendered and positioned relative to its rootElement (typically a trigger button).
  • By default, the menu opens below the trigger and aligns according to the align prop (right by default).
  • The menu automatically adjusts its position to avoid being clipped by the viewport.
  • Keyboard interaction:
    • Arrow keys move focus between menu items.
    • Enter or Space activates the focused item.
    • Escape closes the menu via the close callback.
    • Tabbing away closes the menu via navigateAway.
  • Focus management: When autofocus is enabled, focus moves to the first menu item on open. When the menu closes, focus should return to the trigger (handled by the parent component).
  • The menu is only visible when a valid rootElement is provided.

Accessibility

Menu itself is a structural component. Accessibility is ensured through the patterns that use it (such as ActionMenu and DropdownMenu), but Menu enables those patterns by supporting correct focus and keyboard behavior.

  • All interactions are fully keyboard accessible.
    (WCAG 2.1.1 — Keyboard)
  • Interactive items show a clear visual focus indicator.
    (WCAG 2.4.7 — Focus Visible)
  • Focus is managed predictably when the menu opens and closes.
  • Parent components are responsible for applying correct ARIA roles and relationships (e.g. menu/menuitem, listbox/option, radiogroup/radio).
  • Menu supports use inside complex contexts such as modals without allowing focus to escape unintentionally.

Implementation Examples

Custom Menu with checkboxes

Menu supporting multiple selection with checkboxes.

Code example
import { useRef, useState } from 'react';
import {
Menu as YDSMenu,
MenuContext,
Button,
MenuItem,
MenuSeparator,
} from '@yleisradio/yds-components-react';
import { ChevronDown } from '@yleisradio/yds-icons-react';

export const MenuCheckbox = () => {
const menuButtonRef = useRef(null);
const [menuRoot, setMenuRoot] = useState<HTMLElement | null>(null);
const [selectedItems, setSelectedItems] = useState<string[]>([]);

const toggleItem = (item: string) => {
if (selectedItems.includes(item)) {
setSelectedItems(selectedItems.filter((i) => i !== item));
} else {
setSelectedItems([...selectedItems, item]);
}
};

return (
<>
<Button
ref={menuButtonRef}
variant="text"
removePadding
iconAfter={<ChevronDown />}
onClick={() => {
if (menuRoot) {
setMenuRoot(null);
} else {
setMenuRoot(menuButtonRef.current);
}
}}
>
Menu
</Button>
<MenuContext.Provider value={{ menuSize: 'md' }}>
<YDSMenu id="menu-checkbox" rootElement={menuRoot} buttonRef={menuButtonRef}>
<MenuItem
itemVariant="checkbox"
role="menuitemcheckbox"
onSelect={() => toggleItem('Option 1')}
isSelected={selectedItems.includes('Option 1')}
>
Option 1
</MenuItem>
<MenuItem
itemVariant="checkbox"
role="menuitemcheckbox"
onSelect={() => toggleItem('Option 2')}
isSelected={selectedItems.includes('Option 2')}
>
Option 2
</MenuItem>
<MenuSeparator />
<MenuItem
itemVariant="checkbox"
role="menuitemcheckbox"
onSelect={() => toggleItem('Option 3')}
isSelected={selectedItems.includes('Option 3')}
>
Option 3
</MenuItem>
</YDSMenu>
</MenuContext.Provider>
</>
);
};

Custom Menu with radio buttons

Menu supporting single selection with radio buttons.

Code example
import { useRef, useState } from 'react';
import {
Menu as YDSMenu,
MenuContext,
Button,
MenuItem,
MenuSeparator,
} from '@yleisradio/yds-components-react';
import { ChevronDown } from '@yleisradio/yds-icons-react';

export const MenuRadio = () => {
const menuButtonRef = useRef(null);
const [menuRoot, setMenuRoot] = useState<HTMLElement | null>(null);
const [selectedItem, setSelectedItem] = useState('Option 1');

return (
<>
<Button
ref={menuButtonRef}
variant="text"
removePadding
iconAfter={<ChevronDown />}
onClick={() => {
if (menuRoot) {
setMenuRoot(null);
} else {
setMenuRoot(menuButtonRef.current);
}
}}
>
Menu
</Button>
<MenuContext.Provider value={{ menuSize: 'md' }}>
<YDSMenu id="menu-radio" rootElement={menuRoot} buttonRef={menuButtonRef}>
<MenuItem
itemVariant="radio"
role="menuitemradio"
onSelect={() => setSelectedItem('Option 1')}
isSelected={selectedItem === 'Option 1'}
>
Option 1
</MenuItem>
<MenuItem
itemVariant="radio"
role="menuitemradio"
onSelect={() => setSelectedItem('Option 2')}
isSelected={selectedItem === 'Option 2'}
>
Option 2
</MenuItem>
<MenuSeparator />
<MenuItem
itemVariant="radio"
role="menuitemradio"
onSelect={() => setSelectedItem('Option 3')}
isSelected={selectedItem === 'Option 3'}
>
Option 3
</MenuItem>
</YDSMenu>
</MenuContext.Provider>
</>
);
};

API Reference

Props

The Menu component accepts the following props:

PropTypeRequiredDefaultDescription
idstringYesUnique identifier for the menu
rootElementHTMLElement | nullYesElement to position menu relative to
autofocusbooleanYesWhether to focus first item on open
close() => voidYesCallback to close the menu
buttonRefRefObject<HTMLButtonElement>YesReference to trigger button
navigateAway() => voidYesCallback when navigating away
align'left' | 'right'No'right'Horizontal alignment of menu
childrenReact.ReactNodeYesMenu items to display

Type Definitions

export interface MenuProps {
id: string;
rootElement: HTMLElement | null;
autofocus: boolean;
close: () => void;
buttonRef: RefObject<HTMLButtonElement>;
navigateAway: () => void;
align?: 'left' | 'right';
children: ReactNode;
}

export const Menu = forwardRef<HTMLUListElement, MenuProps>;

The Menu component uses MenuContext to access menu configuration:

export interface MenuInterface {
closeMenu?: () => void;
menuSize: MenuSize;
}

export const MenuContext = createContext<MenuInterface>({ menuSize: 'md' });

Menu items can access the context to:

  • Get the current menu size (md or sm)
  • Access the closeMenu function if needed

Props

The MenuItem component accepts all standard HTML <li> attributes in addition to the following props:

PropTypeRequiredDefaultDescription
iconReactElement | nullNoundefinedIcon element to display before the label
iconHighlightbooleanNofalseWhether to highlight icon area when selected
isDisabledbooleanNofalseWhether the menu item is disabled
isSelectedbooleanNofalseWhether the menu item is selected
itemVariant'default' | 'radio' | 'checkbox'No'default'Visual variant of the menu item
onSelect() => voidNoCallback fired when item is selected
closeMenuOnClickbooleanNotrueWhether to close menu when item is selected
rolestringNo'menuitem'ARIA role for the menu item
childrenReact.ReactNodeYesLabel text or content to display

Type Definitions

export interface MenuItemDSProps {
icon?: ReactElement | null;
iconHighlight?: boolean;
isDisabled?: boolean;
isSelected?: boolean;
closeMenuOnClick?: boolean;
onSelect?: () => void;
role?: string;
children: ReactNode;
itemVariant?: 'default' | 'radio' | 'checkbox';
}

export type MenuItemProps = MenuItemDSProps & HTMLAttributes<HTMLLIElement>;

MenuSeparator is a simple component used to visually separate menu items:

export const MenuSeparator = (): JSX.Element;
Code example
import { MenuItem, MenuSeparator } from '@yleisradio/yds-components-react';

<MenuItem onSelect={() => {}}>Option 1</MenuItem>
<MenuItem onSelect={() => {}}>Option 2</MenuItem>
<MenuSeparator />
<MenuItem onSelect={() => {}}>Option 3</MenuItem>
  • ActionMenu – Uses Menu internally for action-based menus
  • DropdownMenu – Uses Menu internally for selection-based menus
  • DropdownMenuGroup – Uses Menu internally for grouping multiple dropdown menus into a unified control area.
  • ComboboxSingleSelect uses Menu internally for the dropdown menu.