lab · exploring

Menu anatomy

The parts of a menu panel laid out statically open, side by side: section-title spacing at every size, title alignment, separator styles, rich rows, deep submenus, danger and loud checked rows, scroll behaviour, Select and Combobox sections, the searchable menu (ghost header, deep results, no results), and sketches of toggle and trailing-check rows.

frameworksastro
Exploringmenulistboxselectcombobox
scenarioSection titles at every size

Left: the title flush (0) against the separator above, as it shipped. Right: 4px of margin-top (3xs). The title should read as belonging to the rows below it, never to the separator above, at sm, md and lg.

sm

Before · title margin 0
After · title margin-top 3xs (4px)
Recipe default (whatever ships now)

md

Before · title margin 0
After · title margin-top 3xs (4px)
Recipe default (whatever ships now)

lg

Before · title margin 0
After · title margin-top 3xs (4px)
Recipe default (whatever ships now)

Live

scenarioTitle alignment against row content

Titles sit on the affix inset (closer to the edge than row labels). With leading icons or a check column the title should line up with the icon/check column; on plain rows it sits a little left of the label. Does that read as intended or as misaligned?

Plain rows
Leading icons (Stamp ghost)
Check column (checkbox rows)
Check group above a plain group

Live

scenarioSeparators: spacing, edge to edge, inset

Same file-manager menu four ways. Does a titled group still need a rule, or is the title + gap enough? Edge-to-edge is the current default; inset aligns the rule with the title inset.

No separators (titles only)
Spacing only (transparent 2xs separator)
Edge to edge (default)
Inset to the title axis

Live

scenarioUntitled, single-item and long-titled groups

An untitled first group should start at the list's own padding (no phantom title gap). A one-row group should not look orphaned. A long title widens the menu today (nowrap); the truncated variant caps the panel at 22rem and ellipsises the title.

First group untitled
Single-item groups
Long title (grows the panel)
Long title, truncated (container max 22rem)

Live

scenarioRich rows

Leading Stamp, description, shortcut, a Beta badge and a disabled 'Soon' row in one menu. The `shortcut` field renders a KbdGroup sized to the row; the four panels show `shortcutAppearance` soft (default — quiet square keys on a subtle fill, no border or frame), framed (one frame, a cap per key), separate (loose caps) and plain (text with a clear gap between keys). Shortcuts use `mod`, so they read ⌘ on a Mac and Ctrl elsewhere. Every shortcut must sit vertically centred on the row and never taller than the row's affix box. The disabled row must stay legible and its badge must not look clickable.

shortcutAppearance · soft (default)
shortcutAppearance · framed
shortcutAppearance · separate
shortcutAppearance · plain

Live

scenarioDanger group and a loud checked row

Danger rows paint in the danger palette and the group title stays neutral. With activeEmphasis='primary' the chosen workspace is a filled row; with showCheck off the labels sit flush.

Danger group
Default checked row
activeEmphasis primary
primary + primary feedback, showCheck off

Live

scenarioScroll: overflow, fade, sticky titles, flip

(a) A list 3px taller than its cap: is the scrollbar + gutter worth it, or should the cap absorb a few px? (b) Fade masks at the scrolled edges (pure CSS, scroll-driven; absent in browsers without animation-timeline). (c) Sticky vs scrolling group titles. Live: open the long menu with its trigger near the bottom of the viewport (flip vs squeeze, ~3 row minimum), open 'More locations' after scrolling the parent (does the submenu follow or close?), and compare the root panel's arrow — Menu exposes no `arrow` prop, so arrow off is an open question.

(a) Overflows by 3px
(a) Fits exactly
(b) Fade masks, scrolled to the middle
(c) Titles scroll away
(c) Sticky titles (passThrough)

Scroll the page until these triggers sit near the bottom edge, then open them.

Live

scenarioSelect and Combobox sections

The title spacing lives on ListBox's groupLabel, so Select and Combobox sections change too. Before/after on the bare listbox, then the live controls: section titles must still sit closer to their rows than to the previous section.

Before · title margin 0
  • Americas
  • Europe
  • Asia–Pacific
After · title margin-top 3xs
  • Americas
  • Europe
  • Asia–Pacific

Live

  • Americas
  • Europe
  • Asia–Pacific
  • Americas
  • Europe
  • Asia–Pacific
scenarioOpen questions: toggles, trailing check

Sketches of what the API does not support yet. Toggle rows: a switch drawn in the trailing slot (inert, decorative) over a menuitemcheckbox with showCheck off — should Menu get a `switch` appearance instead? Trailing check: showCheck off plus a Check Stamp in the trailing slot of the chosen row — should the check column be placeable at the end?

Toggle rows (sketch)
Trailing check (sketch)
Leading check (today)

Live

scenarioSearch: ghost header, deep results, no results

The search header (menu `search`) sits above the list inside one panel: a borderless field spanning the full width, the search glyph in a ghost Stamp, the text starting on the row-label column, then a full-bleed 1px rule (the listbox separator token). The list's top corners are square; only the list scrolls. Typing flattens every level into one ranked list: a submenu match shows its path as the row eyebrow ('Share › Email'), a disabled match stays disabled, and no match swaps the list for a role=status row OUTSIDE role=menu. Compare at sm, md and lg; then open the live menu (focus lands in the field; ↓/↑ + Enter, → into a submenu, Esc clears then closes).

sm

Empty query · the normal menu
“mail” · deep matches with their path
“pr” · includes a disabled deep match
“zzz” · no results (status outside the menu)
No commands

md

Empty query · the normal menu
“mail” · deep matches with their path
“pr” · includes a disabled deep match
“zzz” · no results (status outside the menu)
No commands

lg

Empty query · the normal menu
“mail” · deep matches with their path
“pr” · includes a disabled deep match
“zzz” · no results (status outside the menu)
No commands
Capped at 220px · the header stays, only the list scrolls

Live