Menus

mandatory

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).

Spec — Menus

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

3 variants

Variant Trigger Use
*ropdown* Tap / click a button (icon / overflow ⋮ / text) List of actions or selection options
*ontext* Longpress (touch) or rightclick (desktop) Operations on the current target
*xposed dropdown* Tap a text field Selection 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   ›  │
                     └──────────────────┘
  • *idth* min 112 dp; max 320 dp; auto-fit to longest item
  • *tem height* 48 px (single-line label)
  • *tem padding* 16 px horizontal, 12 px vertical
  • *ontainer radius* 4 px (Material 3 default)
  • *ontainer bg* surface-container
  • *levation* 2 dp (shadow + tonal overlay)
  • *tem label* body-large
  • *eading icon* 24 px, optional
  • *railing* keyboard shortcut OR submenu chevron (›)
  • *ivider* inset divider between item groups (optional)

R1 — Anchor / positioning

Menu opens relative to trigger:

Anchor Position
Below trigger Default for dropdowns
Above trigger When below would overflow viewport
Right of trigger Submenu (cascading)
Left of trigger Submenu RTL
At cursor Context menu (right-click)

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

R2 — Items

Item type Use
*ction* Label + optional icon + optional shortcut → performs action and closes menu
*oggle* Label + check mark on selected → toggles state, may stay open or close (document)
*roup label* Subheader text, non-interactive
*ivider* Separates groups (full-width inside menu)
*ubmenu* Label + trailing › → opens cascading submenu
*isabled* 38% 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:

OS Format
macOS ⌘C (symbol)
Windows / Linux Ctrl+C (text)
Cross-platform binding Show 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 *nstead of*native <select> for branded Koder UIs.

R7 — States

State Visual
Item rest Base
Item hover State layer 8% on item
Item pressed State layer 12%
Item focused (keyboard) 2 px focus ring on item, OR row tint
Item selected (toggle) Trailing check icon
Item disabled 38% opacity

Open / close animation:

  • *pen* scale 0.95 → 1 from anchor edge + fadein (motionfast)
  • *lose* scale 1 → 0.95 + fadeout (motionfast)
  • Reduced motion: instant in / out

R8 — Dismiss behaviors

Trigger Result
Tap an action item Closes menu, runs action
Tap a toggle item Toggles state, may stay open
Tap outside menu Closes menu, no action
Esc key Closes menu
Scroll page (some surfaces) Closes menu
Resize window Repositions 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

Density Item height Padding
Compact 36 px 12 px / 8 px
Default 48 px 16 px / 12 px
Comfortable 56 px 20 px / 16 px

R11 — Per-preset variation

Preset Container Items
material3 4 px radius, 2 dp elevation, surface-container Tonal hover
material2 4 px radius, 8 dp shadow, white bg Filled hover
ios_cupertino 14 px radius, blur backdrop Solid pressed bg
gnome 8 px radius, surface tonal Solid blue hover
windows_11 Mica backdrop, 8 px radius Accent bar on focus
brutalist Sharp, thick black border, no shadow Inverted hover
terminal_classic Bordered 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

Source: ../home/koder/dev/koder/meta/docs/stack/specs/components/menus.kmd