Pular para o conteúdo

Menus

components specs/components/menus.kmd

Floating list of choices anchored to a trigger — opens on demand, closes after a choice or dismiss. Material parity (`/components/menus`). Covers dropdown (anchor button), context (long-press / right-click), submenu (cascading), and exposed dropdown menu (text field with selection list).

Quando esta spec se aplica

Triggers primários

Todos os triggers

Corpo da especificação

Spec — Menus

Facet Visual of Koder Design. Material parity: https://m3.material.io/components/menus.

3 variants

VariantTriggerUse
DropdownTap / click a button (icon / overflow ⋮ / text)List of actions or selection options
ContextLong-press (touch) or right-click (desktop)Operations on the current target
Exposed dropdownTap a text fieldSelection from a closed list (replaces native <select>)

Pick by trigger type. Dropdown is the default.

Anatomy

                     ┌──────────────────┐
                     │  ✂   Cut    ⌘X   │
   Trigger ───→      │  ⏎   Copy   ⌘C   │
   (button or field) │  📋  Paste  ⌘V   │
                     │  ──────────────  │
                     │  ⚙   Settings    │
                     │  ❌  Delete   ›  │
                     └──────────────────┘
  • Width: min 112 dp; max 320 dp; auto-fit to longest item
  • Item height: 48 px (single-line label)
  • Item padding: 16 px horizontal, 12 px vertical
  • Container radius: 4 px (Material 3 default)
  • Container bg: surface-container
  • Elevation: 2 dp (shadow + tonal overlay)
  • Item label: body-large
  • Leading icon: 24 px, optional
  • Trailing: keyboard shortcut OR submenu chevron (›)
  • Divider: inset divider between item groups (optional)

R1 — Anchor / positioning

Menu opens relative to trigger:

AnchorPosition
Below triggerDefault for dropdowns
Above triggerWhen below would overflow viewport
Right of triggerSubmenu (cascading)
Left of triggerSubmenu RTL
At cursorContext menu (right-click)

Position adjusts at runtime to stay inside viewport — flip / shift within 8 px of edge.

R2 — Items

Item typeUse
ActionLabel + optional icon + optional shortcut → performs action and closes menu
ToggleLabel + check mark on selected → toggles state, may stay open or close (document)
Group labelSubheader text, non-interactive
DividerSeparates groups (full-width inside menu)
SubmenuLabel + trailing › → opens cascading submenu
Disabled38% opacity, not interactive

Max ~10 items recommended; > 10 → use exposed-dropdown style with search (combobox).

R3 — Keyboard shortcut display

Trailing shortcut is label-small (11/16), text-muted, right-aligned:

OSFormat
macOS⌘C (symbol)
Windows / LinuxCtrl+C (text)
Cross-platform bindingShow OS-specific format detected at runtime

Don't show shortcut on touch-only surfaces — useless and clutters.

R4 — Submenu (cascading)

Submenu opens to the right (or left in RTL):

  • Trigger item has trailing chevron (›)
  • Hover / focus on trigger item: submenu opens after 200 ms delay
  • Submenu items follow same anatomy as parent menu
  • Esc closes only the deepest submenu; outside click closes everything
  • Max depth: 2 levels (parent → submenu → no further nesting)

R5 — Context menu

Triggered by:

  • Right-click (desktop)
  • Long-press (touch, ~500 ms)
  • Keyboard Menu key / Shift+F10

Opens at the cursor / touch position, NOT anchored to a trigger element.

Common content:

  • Cut / Copy / Paste (text fields)
  • Select all / Clear (lists)
  • Open / Rename / Delete (file items)
  • Inspect (web)

R6 — Exposed dropdown menu (combobox replacement)

Text field that opens a menu of options on focus / tap:

   ┌──────────────────────────┐
   │  Country        ⌄         │
   └──────────────────────────┘
           ↓ (tap)
   ┌──────────────────────────┐
   │  Country        ⌃         │
   │  ──────────────────────   │
   │  🇧🇷  Brazil               │
   │  🇺🇸  United States        │
   │  🇨🇦  Canada               │
   │  ...                      │
   └──────────────────────────┘
  • Trigger looks like an Outlined or Filled text field with trailing chevron
  • Menu opens below; max-height ~280 dp, scrolls
  • Typing in field filters options (combobox mode); pure menu mode doesn't filter
  • Selecting an option fills the field text and closes the menu

Use this instead of native <select> for branded Koder UIs.

R7 — States

StateVisual
Item restBase
Item hoverState layer 8% on item
Item pressedState layer 12%
Item focused (keyboard)2 px focus ring on item, OR row tint
Item selected (toggle)Trailing check icon
Item disabled38% opacity

Open / close animation:

  • Open: scale 0.95 → 1 from anchor edge + fade-in (motion-fast)
  • Close: scale 1 → 0.95 + fade-out (motion-fast)
  • Reduced motion: instant in / out

R8 — Dismiss behaviors

TriggerResult
Tap an action itemCloses menu, runs action
Tap a toggle itemToggles state, may stay open
Tap outside menuCloses menu, no action
Esc keyCloses menu
Scroll page (some surfaces)Closes menu
Resize windowRepositions or closes

R9 — Accessibility

  • Trigger: aria-haspopup="menu" + aria-expanded="true" when open
  • Menu container: role="menu"
  • Each item: role="menuitem" (action), role="menuitemcheckbox" (toggle), role="menuitemradio" (single-select group)
  • Submenu trigger: aria-haspopup="menu" + aria-expanded
  • Group label: role="none" on label + role="group" on items group
  • Divider: role="separator"
  • Keyboard:
    • Arrow Down: move to next item (wrap to first)
    • Arrow Up: previous (wrap to last)
    • Arrow Right: open submenu (if focused on submenu trigger)
    • Arrow Left: close submenu
    • Home / End: first / last
    • Enter / Space: activate
    • Esc: close
    • Type-ahead: typing letter focuses next item starting with that letter

R10 — Density

DensityItem heightPadding
Compact36 px12 px / 8 px
Default48 px16 px / 12 px
Comfortable56 px20 px / 16 px

R11 — Per-preset variation

PresetContainerItems
material34 px radius, 2 dp elevation, surface-containerTonal hover
material24 px radius, 8 dp shadow, white bgFilled hover
ios_cupertino14 px radius, blur backdropSolid pressed bg
gnome8 px radius, surface tonalSolid blue hover
windows_11Mica backdrop, 8 px radiusAccent bar on focus
brutalistSharp, thick black border, no shadowInverted hover
terminal_classicBordered box, single line per item> prefix on focus

R12 — Forbidden patterns

  • ❌ Menus that scroll horizontally
  • ❌ Menus wider than 320 dp (too wide; use dialog or card)
  • ❌ Items wrapping to multiple lines (truncate or shorten label)
  • ❌ Submenu depth > 2 levels
  • ❌ Menu that re-opens automatically after closing
  • ❌ Menu without keyboard support
  • ❌ Context menu with > 12 items at top level (overwhelming)
  • ❌ Menu items as form controls (text fields, sliders) — use dialog or popover instead
  • ❌ Confirmation dialogs from menu items without warning (destructive actions should be visually marked as destructive — error color)
  • themes/elevation.kmd — 2 dp tonal + shadow recipe
  • themes/color-roles.kmdsurface-container background
  • themes/typography.kmdbody-large for items, label-small for shortcuts
  • interaction/states.kmd — hover / focused / pressed layers
  • components/lists.kmd — sibling pattern (lists are persistent menus)
  • components/text-fields.kmd — exposed dropdown anchor
  • foundations/elements.kmd — Container + Control families

Requirements (testable)

Requirement: Anchor positioning stays inside the viewport {#req-menus-anchor-positioning}

A menu SHALL open anchored below its trigger by default, and SHALL flip above (or shift horizontally) at runtime whenever the default placement would overflow the viewport, keeping the container within 8 px of any edge.

Scenario: Dropdown opens below its trigger

  • GIVEN a dropdown trigger with ample space beneath it
  • WHEN the trigger is activated
  • THEN the menu container's top edge is positioned adjacent to the trigger's bottom edge
  • AND the menu is anchored below the trigger

Scenario: Menu flips above when it would overflow the bottom

  • GIVEN a dropdown trigger near the bottom of the viewport where the menu height exceeds the space below it
  • WHEN the trigger is activated
  • THEN the menu is rendered above the trigger instead of below
  • AND the menu's bounding box stays at least 8 px inside every viewport edge

Requirement: Cascading submenu behavior and depth limit {#req-menus-submenu}

A submenu SHALL be marked by a trailing chevron on its parent item, SHALL open to the right (left in RTL) after a 200 ms hover/focus delay, and the menu SHALL NOT nest deeper than 2 levels (parent → submenu).

Scenario: Submenu opens after the hover delay

  • GIVEN an open menu whose item carries a trailing chevron
  • WHEN the pointer hovers that submenu-trigger item for 200 ms
  • THEN the submenu opens positioned to the right of the parent item
  • AND the parent item's trailing chevron remains visible

Scenario: Esc closes only the deepest submenu

  • GIVEN a parent menu with an open submenu
  • WHEN the Esc key is pressed once
  • THEN the deepest submenu closes
  • AND the parent menu remains open

Scenario: Nesting is capped at two levels

  • GIVEN a submenu already opened from a parent menu
  • WHEN a submenu item that would open a third-level menu is authored
  • THEN no third-level menu is produced
  • AND the maximum realized depth is 2

Requirement: Context menu opens at the cursor {#req-menus-context-position}

A context menu SHALL be invoked by right-click, ~500 ms long-press, or the keyboard Menu key / Shift+F10, and SHALL open at the cursor or touch position rather than anchored to a trigger element.

Scenario: Right-click opens the menu at the pointer

  • GIVEN a surface that exposes a context menu
  • WHEN the user right-clicks at coordinates (x, y)
  • THEN the context menu opens with its top-left origin at (x, y), adjusted only to stay inside the viewport
  • AND the menu is not anchored to any trigger button

Scenario: Long-press opens the context menu on touch

  • GIVEN a touch surface with a context menu
  • WHEN the user presses and holds for ~500 ms without moving
  • THEN the context menu opens at the touch position

Requirement: Exposed dropdown replaces native select {#req-menus-exposed-dropdown}

An exposed dropdown SHALL render as a text field with a trailing chevron, SHALL open a scrolling menu of options below the field on focus/tap, and selecting an option SHALL fill the field text and close the menu.

Scenario: Selecting an option fills the field and closes

  • GIVEN a closed exposed dropdown showing a trailing down-chevron
  • WHEN the user taps the field and then selects the option "Brazil"
  • THEN the field text becomes "Brazil"
  • AND the option menu closes

Requirement: Dismiss behaviors {#req-menus-dismiss}

An open menu SHALL close when an action item is activated (also running its action), when the user taps outside the menu (running no action), and when the Esc key is pressed.

Scenario: Tapping an action item runs it and closes

  • GIVEN an open menu with an enabled action item
  • WHEN the action item is tapped
  • THEN the item's action is invoked exactly once
  • AND the menu closes

Scenario: Tapping outside closes without acting

  • GIVEN an open menu
  • WHEN the user taps a point outside the menu container
  • THEN the menu closes
  • AND no menu item action is invoked

Requirement: Accessibility roles and keyboard navigation {#req-menus-a11y}

A menu SHALL expose role="menu" on its container with role="menuitem" (action), role="menuitemcheckbox" (toggle), or role="menuitemradio" (single-select) on items, its trigger SHALL carry aria-haspopup="menu" with aria-expanded reflecting open state, and arrow-key navigation SHALL move roving focus between items wrapping at the ends.

Scenario: Trigger exposes popup state

  • GIVEN a menu trigger in its closed state
  • WHEN the menu is opened
  • THEN the trigger reports aria-haspopup="menu"
  • AND the trigger's aria-expanded attribute reads "true"

Scenario: Arrow-down navigation wraps

  • GIVEN an open menu with keyboard focus on the last item
  • WHEN the Arrow Down key is pressed
  • THEN focus moves to the first item
  • AND that item receives the keyboard focus indicator

Scenario: Esc closes the menu from the keyboard

  • GIVEN an open menu with a focused item
  • WHEN the Esc key is pressed
  • THEN the menu closes
  • AND aria-expanded on the trigger reads "false"

Referências