Menu

Menu is the concise flat command list. MenuContent is the retained anatomy API for product menus that need groups, labels, separators, descriptions, trailing information, explicit disabled states, or destructive emphasis.
Use either form for commands such as rename, archive, duplicate, or export. Use Select when rows represent one persistent value instead. MenuButton adds an anchored trigger, controlled open state, dismissal, and popup focus behavior to the flat model.

Flat menu

use fission::prelude::*;

let menu: Widget = Menu {
    items: vec![
        MenuItem {
            label: "Archive".into(),
            icon: None,
            on_select: Some(archive_action),
            semantics_identifier: Some("message.archive".into()),
        },
        MenuItem {
            label: "Mark unread".into(),
            icon: None,
            on_select: Some(mark_unread_action),
            semantics_identifier: Some("message.mark-unread".into()),
        },
    ],
    width: Some(220.0),
    max_height: Some(280.0),
}
.into();
Menu adapts every MenuItem into the same themed and semantic row used by the richer API, so the convenience form does not create a second visual model.

Composed menu anatomy

use fission::prelude::*;

let project_menu: Widget = MenuContent::new(vec![
    MenuGroup::new(vec![
        MenuActionItem::new("Open project")
            .leading_icon(open_icon_svg)
            .description("Open in the current workspace")
            .shortcut("Ctrl+O")
            .on_select(open_project_action)
            .semantics_identifier("project.open")
            .into(),
        MenuActionItem::new("Move project")
            .metadata("Unavailable offline")
            .on_select(move_project_action)
            .disabled(!can_move_project)
            .semantics_identifier("project.move")
            .into(),
    ])
    .label("Project")
    .into(),
    MenuSeparator::new().into(),
    MenuActionItem::new("Delete project")
        .tone(MenuItemTone::Destructive)
        .on_select(delete_project_action)
        .semantics_identifier("project.delete")
        .into(),
])
.id(WidgetId::explicit("project-actions.content"))
.width(244.0)
.max_height(300.0)
.into();
The public anatomy types have distinct responsibilities:
Type
Responsibility
MenuContent
Owns one vertical Menu or ListBox surface, width, height bound, scrolling, and composite semantics.
MenuEntry
Retains an action item, group, standalone label, or separator in display order. Each anatomy type converts into it.
MenuActionItem
Owns an actionable row with label, optional description and leading icon, one trailing value, selection, disabled state, and tone.
MenuGroup
Groups related entries and can expose a visible MenuLabel as its semantic label.
MenuLabel
Renders a visible, non-focusable heading with optional stable identity and semantic identifier.
MenuSeparator
Renders a retained, non-focusable visual boundary between regions.
MenuActionItem::shortcut(...) and .metadata(...) both set the same trailing slot, so the later call wins rather than producing competing content. If any row has a leading icon or selected state, the surface reserves that column for all action rows, including rows nested in groups. Labels therefore stay aligned instead of shifting from item to item.
MenuContent::list_box(...) creates the option-role form used by fixed and editable choice controls. MenuActionItem::option(label, selected) makes the state explicit; the list-box constructor also normalizes ordinary action items to unselected options. Most applications should use Select or Combobox rather than assembling that lower-level relationship themselves.

Field and builder reference

Menu fields:
Field
Type
Meaning
Default
items
Vec<MenuItem>
Flat commands in visual and keyboard order.
Required.
width
Option<f32>
Logical surface width.
The active menu surface recipe, 208 points in the default design.
max_height
Option<f32>
Maximum surface height before scrolling.
300 points when omitted.
MenuContent exposes id, entries, width, and max_height directly, with matching builders. new(...) creates a command menu. list_box(...) creates option semantics and leaves width available for an anchoring flyout to resolve. The advanced .fluid_width(...), .dismiss_action(...), and .focus_barrier(...) builders let a composite owner coordinate retained popup width, semantic dismissal, and focus containment; they do not create a trigger or outside-click layer by themselves.
MenuActionItem starts as a normal, enabled command with only its label. Its builders add:
Builder / field
Meaning
id(...)
Explicit retained identity; otherwise the containing surface derives one.
description(...)
Supporting line related to the action through described_by.
leading_icon(...)
SVG at the logical leading edge.
shortcut(...) / metadata(...)
Mutually exclusive trailing content.
on_select(...)
Action dispatched by the enabled row.
semantics_identifier(...)
Stable accessibility and semantic-test identifier.
selected(...)
Explicit selected state and shared indicator column.
disabled(...)
Visible unavailable state with focus and activation removed.
tone(...)
Normal or Destructive semantic visual treatment.

Design-system states

The complete anatomy resolves from theme.components.menu. The surface recipe owns fill, border, radius, width, padding, gap, and elevation. Separate recipes own normal and destructive item states, descriptions, shortcuts, metadata, group labels, separators, and the selected indicator.
Action rows resolve default, hover, active, focus, disabled, and selected states. A description can make a row taller than the recipe's minimum item height. The Destructive tone changes presentation but not reducer behavior; the application still owns confirmation and safety rules.

Keyboard and semantics

A command surface exposes a vertical Menu role and its action rows expose MenuItem. A list-box surface exposes ListBox and Option instead. Groups expose Group and relate to their visible label through labelled_by. Descriptions are related to their action item through described_by, and separators expose Separator. Labels and separators never become focus stops.
Only one enabled row participates in page-level Tab traversal: the first enabled command, or the selected enabled option when a list box has one. Once focus is inside, ArrowUp and ArrowDown move through enabled rows and wrap; Home and End reach the boundaries. Typing one unmodified character moves to the next label beginning with that character. This is single-character matching, not buffered multi-character search. Enter and Space activate the focused row.
A disabled action item remains visible, exposes its disabled state, receives no focus, and dispatches no action. By contrast, a legacy MenuItem with on_select: None has not declared itself disabled: it remains a focusable row without a default action. Prefer MenuActionItem::disabled(true) when an unavailable state must remain visible.
A standalone surface does not own an anchor or outside-click dismissal. .dismiss_action(...) supplies the semantic action used by Escape and equivalent input; .focus_barrier(true) contains focus while the surface is mounted. Use the high-level composite or a deliberately assembled Popover when those responsibilities belong to an anchored control.

Responsive, direction, target, and motion behavior

The surface uses its configured width or the design-system width rather than choosing a breakpoint. When estimated content exceeds max_height, it clamps the viewport and enables a scrollbar; it does not virtualize a very large data set. Test long translations, descriptions, shortcuts, and metadata within the real parent constraint.
Leading and trailing anatomy and row order mirror with the active layout direction. Paragraph bidi and shaping remain governed independently by the text system's TextDirection.
Menu surfaces register no motion themselves. Any motion composed around a menu still follows the global Env::motion_preference, so they need no widget-specific preference branch. Their visual and semantic structure lowers through the shared target model. Static site and plain SSR output can render it, but reducer actions and popup state need an interactive runtime or browser island.

Production checklist

•
Use short verb-led labels and reserve descriptions for information that changes the decision.
•
Use the explicit disabled state instead of leaving an apparently available command silently inert.
•
Verify arrow wrapping, boundary keys, one-character matching, activation, disabled-row skipping, and scrolling with the final content.
•
Give dynamic, reordered, or localized entries stable identities or semantic identifiers.
•
Use a searchable or virtualized surface when the command set is genuinely large.
Previous
Hero