MenuPanel
The shared menu surface component that renders menu items, group headers, and nested submenu cards.
import { MenuPanel } from "@glint/components/menu-panel.slint";import { MenuEntry, MenuTone } from "@glint/components/menu-entry.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 560px; height: 340px; background: Tokens.color-background;
in-out property <string> last-selected: "None";
VerticalLayout { alignment: center; spacing: 12px; padding: 24px;
HorizontalLayout { alignment: center;
MenuPanel { items: [ { label: "Edit Item", icon: IconSet.Pencil, shortcut: "Ctrl+E" }, { label: "Share", icon: IconSet.Share2, children: [ { label: "Copy Link", shortcut: "Ctrl+C" }, { label: "Email Invite" }, ]}, { label: "Archive", separator-before: true, heading-before: "Actions", icon: IconSet.Archive }, { label: "Delete", icon: IconSet.Trash2, tone: MenuTone.destructive }, ]; selected(row, child) => { if (child >= 0) { root.last-selected = "Submenu item " + child + " in row " + row; } else { root.last-selected = "Row " + row; } } } }
Text { text: "Selected: " + root.last-selected; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-sm-size; horizontal-alignment: center; } }}Usage
import { MenuPanel } from "@glint/components/menu-panel.slint";import { MenuEntry } from "@glint/components/menu-entry.slint";
export component AppWindow inherits Window { VerticalLayout { alignment: center;
MenuPanel { items: [ { label: "Profile" }, { label: "Settings" }, ]; selected(row, child) => { // handle pick } } }}MenuPanel is the core surface component behind DropdownMenu, ContextMenu, and Menubar.
It renders the styled card, item list, group headers, checkmarks, and nested submenu cards in a single coordinate system.
The model is defined by MenuEntry, MenuSubEntry, and MenuTone. Grouping headers and dividers follow the unified grouped rows pattern established by Select.
MenuEntry Model
label: string— Display text for the menu item.icon: LucideIcon— Leading icon rendered from@lucide.shortcut: string— Trailing keyboard shortcut description (rendered withKbd).separator-before: bool— Draws a hairline separator above this row.heading-before: string— Renders a non-selectable category heading above this row.tone: MenuTone— Visual tone (MenuTone.defaultorMenuTone.destructive).checkable: bool— When true, renders this item as a toggleable checkbox row.checked: bool— Boolean check state read by the panel. Handlecheckbox-toggledorradio-pickedand write the new state back toitems;MenuRadioSetcan clear sibling radio rows after a radio pick.radio-group: string— Associates checkable items into a mutually exclusive group.children: [MenuSubEntry]— Array of sub-entries; presence of children turns this row into an expandable submenu opener.
MenuSubEntry carries command fields (label, icon, shortcut, separator-before, heading-before, tone) for submenu leaves.
Examples
Submenu layout and alignment
Use submenu-side (PanelSide.right, PanelSide.left) and submenu-align (PanelAlign.start, PanelAlign.center, PanelAlign.end) to position nested submenu cards.
import { MenuPanel } from "@glint/components/menu-panel.slint";import { PanelSide, PanelAlign } from "@glint/components/panel.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 560px; height: 320px; background: Tokens.color-background;
VerticalLayout { alignment: center; padding: 24px;
HorizontalLayout { alignment: center;
MenuPanel { submenu-side: PanelSide.left; submenu-align: PanelAlign.start; min-content-width: 200px; items: [ { label: "Format", icon: IconSet.FileCode, children: [ { label: "Prettify" }, { label: "Minify" }, ]}, { label: "Validate", icon: IconSet.CheckCheck }, ]; } } }}Radio synchronization with MenuRadioSet
MenuRadioSet is an invisible helper that listens to radio selection events and reactively clears sibling radio rows in the same radio-group.
import { MenuPanel, MenuRadioSet } from "@glint/components/menu-panel.slint";import { MenuEntry } from "@glint/components/menu-entry.slint";import { Tokens } from "@glint/theme/tokens.slint";
export component Demo inherits Window { width: 560px; height: 320px; background: Tokens.color-background;
in-out property <[MenuEntry]> entries: [ { label: "Light", checkable: true, checked: false, radio-group: "theme" }, { label: "Dark", checkable: true, checked: true, radio-group: "theme" }, { label: "System", checkable: true, checked: false, radio-group: "theme" }, ]; in-out property <string> active-group; in-out property <int> picked-row: -1; in-out property <int> generation: 0;
MenuRadioSet { items <=> root.entries; group: root.active-group; picked-row: root.picked-row; generation: root.generation; }
VerticalLayout { alignment: center; padding: 24px;
HorizontalLayout { alignment: center;
MenuPanel { items: root.entries; radio-picked(group, row) => { root.entries[row].checked = true; root.active-group = group; root.picked-row = row; root.generation += 1; } } } }}Destructive actions and item tones
Use tone: MenuTone.destructive on actions that delete or irreversibly alter user data. Destructive rows render with highlight styling that reinforces caution.
import { MenuPanel } from "@glint/components/menu-panel.slint";import { MenuTone } from "@glint/components/menu-entry.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 560px; height: 320px; background: Tokens.color-background;
VerticalLayout { alignment: center; padding: 24px;
HorizontalLayout { alignment: center;
MenuPanel { items: [ { label: "Save Copy", icon: IconSet.Save }, { label: "Discard Changes", icon: IconSet.Trash2, tone: MenuTone.destructive }, ]; } } }}API Reference
Properties
| Property | Type | Default | Description |
|---|---|---|---|
items | in [MenuEntry] | no default | The menu's rows. Read-only here: the panel reports what was picked and the menu around it owns the write-back, because a Menubar's rows live two models deep and a binding cannot be aliased into that (ADR-0020). |
min-content-width | in length | 200px | The floor under the card's width. The card grows past it to fit its widest row, gutter, shortcut hint and chevron included. |
highlighted-index | in-out int | 0 | Row the keyboard stands on. |
open-submenu-index | in-out int | -1 | Row whose submenu is open, or -1 for none. |
submenu-highlight | in-out int | 0 | Row the keyboard stands on inside that submenu. |
submenu-side | in PanelSide | PanelSide.right | Which side of the menu a submenu panel opens on, and how it lines up against the row it hangs off. A submenu opens *beside* its row, so top and bottom have nothing to mean here and read as right. The panel never rises above the card's top edge, whatever the alignment asks for: the popup's box starts there. |
submenu-align | in PanelAlign | PanelAlign.start | |
submenu-open | out bool | no default | True while a submenu panel is on screen. |
panel-width | out length | no default | The card's own width: never below the floor, otherwise its widest row. |
card-x | out length | no default | A submenu opening to the left pushes the card right inside the popup's box; the menu around this panel subtracts this from where it opens, so the card itself stays anchored where it was asked for. |
total-width | out length | no default | The whole popup's box — the card, plus the submenu wherever it landed. |
total-height | out length | no default |
Callbacks
| Callback | Description |
|---|---|
selected(int , int) | Fired with the row's index and, when the row taken was a submenu leaf, that leaf's index inside the row's children — otherwise -1. |
checkbox-toggled(int) | Fired when a checkbox row is taken, just before selected, so the consumer can flip it and selected can observe a settled model. |
radio-picked(string , int) | Fired when a radio row is picked, naming its group and its index. The menu around this panel owns the clearing of the group's other rows, because the panel dies with the popup the pick closes (ADR-0020). |
Functions
| Function | Description |
|---|---|
open-sub-at(index: int, y: length, h: length) | Open the submenu of the row at index, whose top edge sits y below the card's and which is h tall. The pointer path calls this: the row that was hovered knows its own geometry. |
open-submenu() | Open the highlighted row's submenu, if it has one. The keyboard path, which has only the geometry the highlighted row last reported. |
can-open-submenu() -> bool | True when the highlighted row has a submenu to open. |
close-submenu() | Close the open submenu, leaving the keyboard on the row that opened it. |
activate-highlighted() | Take the highlighted row, or the highlighted leaf of the open submenu. |
handle-key(text: string) -> bool | The keys a menu's insides own: the highlight, the ends of the list and the activation. Esc and the ←/→ walk stay with the menu around this panel, because what they mean depends on what the menu is — a menubar walks on to its neighbour where a dropdown has nowhere to go. Returns true when the key was the panel's. |
Enums
| Enum | Values |
|---|---|
PanelSide | top, right, bottom, left |
PanelAlign | start, center, end |
MenuRadioSet
An invisible bookkeeping helper that maintains single-choice selection across items with matching radio-group identifiers.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
items | in-out [MenuEntry] | no default | The same rows the menu shows, written through. |
group | in string | no default | The group that was picked, and the row of it that took the pick. |
picked-row | in int | no default | |
generation | in int | no default | Bumped by the menu on every pick; a counter rather than a flag, so two picks in the same group still read as two events. |
Accessibility
- List container.
MenuPanelpresents an accessiblelistrole carryingaccessible-item-countmatching the number of actionable rows. - Row roles. Each item is exposed as a
list-itemcarrying its index, label, checkable state, and shortcut descriptions. - Keyboard navigation.
handle-key(string)processes arrow keys, Home, End, Space, and Enter, dispatching highlight moves and selection activations. - Group headings. Group headers rendered by
GroupOpenerare treated as non-focusable separator labels.