MenuButton

MenuButton combines an outline trigger and an anchored command menu. Use it for a short list of actions whose availability belongs to one control. For a persistent form value, use Select.
Application state owns is_open. The trigger, outside click, Escape, and any item reducer all request state changes through actions; the widget never hides the menu by mutating state internally.

Example

use fission::prelude::*;

let actions: Widget = MenuButton {
    id: WidgetId::explicit("message-actions"),
    label: "Actions".into(),
    items: vec![
        MenuItem {
            label: "Archive".into(),
            icon: None,
            on_select: Some(archive_and_close_action),
            semantics_identifier: Some("message-actions.archive".into()),
        },
        MenuItem {
            label: "Delete".into(),
            icon: None,
            on_select: Some(delete_and_close_action),
            semantics_identifier: Some("message-actions.delete".into()),
        },
    ],
    is_open: view.state().actions_open,
    on_toggle: Some(toggle_actions_menu_action),
    trigger_semantics_identifier: Some("message-actions.trigger".into()),
}
.into();
An item action should update both the product state and is_open when selecting the item is expected to close the menu. Selection alone does not close it.
When a command surface needs groups, headings, separators, descriptions, shortcuts, disabled rows, or destructive emphasis, use the retained layout instead of rebuilding the trigger and popup relationship:
use fission::prelude::*;

let actions: Widget = MenuButtonLayout::new(
    WidgetId::explicit("message-actions"),
    MenuTrigger::new("Actions")
        .size(ComponentSize::Sm)
        .semantics_identifier("message-actions.trigger"),
    MenuContent::new(vec![
        MenuGroup::new(vec![
            MenuActionItem::new("Archive")
                .shortcut("Ctrl+A")
                .on_select(archive_and_close_action)
                .semantics_identifier("message-actions.archive")
                .into(),
        ])
        .label("Message")
        .into(),
        MenuSeparator::new().into(),
        MenuActionItem::new("Delete")
            .tone(MenuItemTone::Destructive)
            .on_select(delete_and_close_action)
            .semantics_identifier("message-actions.delete")
            .into(),
    ])
    .width(224.0),
)
.open(view.state().actions_open)
.on_toggle(toggle_actions_menu_action)
.into();

Anatomy and design-system behavior

The trigger is a start-aligned outline Button containing the label and a chevron. The open portal contains a design-system-width, 300-point-bounded MenuContent surface aligned to the trigger's end with a four-point gap. The default design makes that surface 208 points wide.
The trigger uses the active button recipe. Each compact MenuItem is adapted to MenuActionItem, so row geometry and default, hover, active, focus, and selected presentation come from theme.components.menu; the same recipe also owns popup fill, border, radius, spacing, and elevation. MenuButton keeps its source-compatible flat item model. MenuButtonLayout exposes the same retained recipe through MenuTrigger and MenuContent, while continuing to own the trigger relationship, anchor, focus barrier, dismissal, and controlled state.

Field reference

Field
Type
Meaning
Notes
id
WidgetId
Stable identity for trigger, popup, item identities, and portal anchor.
Required and unique for each retained menu button.
label
String
Visible trigger label.
Also supplies the trigger's semantic name.
items
Vec<MenuItem>
Commands in display and navigation order.
Each item owns its selection action.
is_open
bool
Controlled popup state.
true registers the menu portal.
on_toggle
Option<ActionEnvelope>
Trigger and dismissal action.
Supply it for opening, outside-click dismissal, Escape, and state restoration.
trigger_semantics_identifier
Option<String>
Stable identifier on the generated trigger semantic node.
Does not replace label.
MenuButtonLayout::new(id, trigger, content) is the complete anatomy API. .open(...) supplies controlled visibility and .on_toggle(...) supplies the single action used for trigger activation and dismissal. MenuTrigger owns its accessible label, optional custom retained child, size, disabled state, and semantic identifier. MenuContent owns the popup entries and size bounds.

Keyboard, focus, and semantics

The trigger exposes Button, expanded, has-popup Menu, and controls relationships. The popup exposes a vertical Menu role, a dismiss action, and MenuItem children with stable identities.
Opening creates a focus barrier and moves focus to the first focusable item when one exists. Within the menu, ArrowUp and ArrowDown wrap, Home and End reach the boundaries, and one unmodified character moves to the next label beginning with that character. Matching is single-character, not buffered multi-character search. Enter or Space activates the focused item. Escape dispatches on_toggle and restores focus to the trigger. Tab or reverse Tab dismisses the menu and continues to the next eligible target outside the popup; menu items are not extra page-level Tab stops.
An outside pointer press dispatches the same close action while open. A closed menu installs no input backdrop, so unrelated controls remain clickable and a closed menu cannot dispatch on_toggle as dismissal.

Responsive, target, and motion behavior

The convenience widget intentionally uses the standard popup width and maximum height. Use MenuButtonLayout to set MenuContent dimensions or compose richer entries without taking over popup behavior. Test long translations and narrow viewports because labels share the row with leading and trailing anatomy.
Automatic vertical placement prefers below the trigger, then moves above when the lower placement would overflow and the upper placement fits. Horizontal placement is clamped to the viewport. The popup uses logical End alignment: it anchors to the trigger's right edge in left-to-right layout and its left edge in right-to-left layout.
Layout direction controls row order, alignment, and flyout edges. Paragraph bidi and shaping remain governed independently by the text system's TextDirection; they are not inferred from layout direction.
MenuButton currently configures no popover or item animation. Any motion composed around the controller or popup still follows the global Env::motion_preference, so it needs no separate preference branch. Its visual and semantic model lowers through interactive shells. Static site and plain SSR output do not run the controlled reducer loop without a browser island or equivalent interactive path.

Production checklist

•
Give the trigger and repeated items stable identifiers when tests or accessibility inspection need labels independent of translated text.
•
Verify trigger activation, arrow movement, one-character matching, item activation, Escape, Tab, reverse Tab, outside click, and focus return.
•
Close the menu in item reducers when that is the product expectation.
•
Use a different surface for long, searchable, or comparison-heavy content.
Previous
Menu