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
| | | |
|---|
| | Stable identity for trigger, popup, item identities, and portal anchor. | Required and unique for each retained menu button. |
| | | Also supplies the trigger's semantic name. |
| | Commands in display and navigation order. | Each item owns its selection action. |
| | | true registers the menu portal. |
| | Trigger and dismissal action. | Supply it for opening, outside-click dismissal, Escape, and state restoration. |
trigger_semantics_identifier | | Stable identifier on the generated trigger semantic node. | |
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.