Overlay · DropdownMenu
DropdownMenu
Button-anchored menu — compound API parity with Radix DropdownMenu and
bits-ui. 14 morfo parts + an eidos-only CheckboxGroup
wrapper for Bits-UI-style array binding (15 importable components).
Includes Items, standalone or grouped Checkbox items, RadioGroup /
RadioItem, Group / GroupHeading, Separator, nested Sub / SubTrigger /
SubContent, and an optional Arrow. 3 semantic events.
soma owns positioning,
keyboard navigation, focus trap and dismissal;
eidos paints uniform panel
chrome across levels.
parts {compiled.parts.order.length}
events {events.length}
apg
menu-button ↗
{#if lastSelected}
last {lastSelected}
{/if}
{triggerLabel}
{#if showGroupHeading}
Account
Profile
Settings
{:else}
Profile
Settings
{/if}
{#if showSeparators} {/if}
{#if showCheckboxItem}
Show notifications
{/if}
{#if showCheckboxGroup}
{#if showGroupHeading}View {/if}
Ruler
Grid
Guides
{/if}
{#if showSeparators} {/if}
{#if showRadioGroup}
{#if showGroupHeading}Theme {/if}
Light
Dark
System
{/if}
{#if showSeparators} {/if}
{#if showSub}
{subTriggerLabel}
Export…
Import…
Sync (offline)
{/if}
{#if showSeparators} {/if}
Log out
{#if showArrow} {/if}
trace
{#if trace.length === 0}
click the trigger to summon the menu
{:else}
{#each trace.slice(0, 3) as entry}
{entry.event} · {entry.family}{entry.intent ? ' · ' + entry.intent : ''}
{fmtTime(entry.at)}
{/each}
{/if}
open {String(open)}
{#if lastSelected}
last {lastSelected}
{/if}
(tab = 'live')}>Live
(tab = 'api')}>
API 15
(tab = 'morfo')}>
morfo
{partsList.length}p · {events.length}e
(tab = 'sema')}>
sema
{events.length}
(tab = 'recipe')}>Recipe
(tab = 'a11y')}>A11y
{#if tab === 'live'}
Controls
DropdownMenu is a pure pass-through over soma ;
the eidos layer adds no evaluative props. Knobs are grouped by the
layer that owns the prop, with a composition group for
parts toggled in / out of the markup (per the
composition-over-visibility norm). Semantic events live in the { e.preventDefault(); tab = 'sema'; }}>Sema tab.
soma Provider props
open bindable
{open ? 'open' : 'closed'}
accessibleWhenDisabled
{accessibleWhenDisabled ? 'on' : 'off'}
dir
{#each ['ltr', 'rtl'] as d (d)}
(dir = d as 'ltr' | 'rtl')}>{d}
{/each}
soma Content props
· floating layer · positioning + focus mgmt
side
{#each ['top', 'right', 'bottom', 'left'] as s (s)}
(side = s as typeof side)}>{s}
{/each}
sideOffset {sideOffset}px
align
{#each ['start', 'center', 'end'] as a (a)}
(align = a as typeof align)}>{a}
{/each}
alignOffset {alignOffset}px
avoidCollisions
{avoidCollisions ? 'on' : 'off'}
loop
{loop ? 'on' : 'off'}
trapFocus
{trapFocus ? 'on' : 'off'}
preventScroll
{preventScroll ? 'on' : 'off'}
soma Item props
· applied to the Log out item below
disabled
{disableLogout ? 'on' : 'off'}
closeOnSelect
{closeOnSelect ? 'on' : 'off'}
Composition
· toggle parts in / out of the markup
Group + GroupHeading
{showGroupHeading ? 'on' : 'off'}
Separators
{showSeparators ? 'on' : 'off'}
CheckboxItem standalone bind:checked
{showCheckboxItem ? 'on' : 'off'}
CheckboxGroup array binding
{showCheckboxGroup ? 'on' : 'off'}
RadioGroup
{showRadioGroup ? 'on' : 'off'}
Sub menu
{showSub ? 'on' : 'off'}
Arrow
{showArrow ? 'on' : 'off'}
eidos Trigger
· visual props forwarded to the underlying <Button>
variant
{#each ['surface', 'outline', 'ghost'] as v (v)}
(triggerVariant = v as typeof triggerVariant)}
>
{v}
{/each}
size
{#each ['xs', 'sm', 'md', 'lg', 'xl'] as s (s)}
(triggerSize = s as typeof triggerSize)}
>
{s}
{/each}
intent
{#each ['neutral', 'affirm', 'fulfill', 'risk', 'threat', 'loss'] as i (i)}
(triggerIntent = i as typeof triggerIntent)}
>
{i}
{/each}
Bound state
· `bind:checked` / `bind:value` mirrors from the live menu items
notifications CheckboxItem
{notifications ? 'on' : 'off'}
viewLayers CheckboxGroup array
theme RadioGroup
{#each ['light', 'dark', 'system'] as t (t)}
(theme = t as typeof theme)}>{t}
{/each}
Demo content
· literal labels rendered into the menu
Trigger label
SubTrigger label
soma
headless · positioning + keyboard + dismissal
svelte
{somaSnippet}
eidos
visual · namespaced compound (pure pass-through; no extra props)
svelte
{eidosSnippet}
{/if}
{#if tab === 'api'}
API reference
15 parts forwarded from soma. Item-family parts share
id, disabled, textValue,
closeOnSelect. CheckboxGroup adds a Bits-UI-style
array binding (item is checked when its value is in
the group's array).
DropdownMenu (Provider)
Prop Type Default Description
id string auto Stable identifier.
open bindable boolean false Whether the menu is open.
onOpenChange / onOpenChangeComplete (open: boolean) => void — Fire on every open/close and after the animation settles.
dir 'ltr' | 'rtl' 'ltr' Direction used for keyboard navigation inside sub-menus.
accessibleWhenDisabled boolean true Disabled items remain focusable per WAI-ARIA APG; set false to skip them entirely.
Trigger
Prop Type Default Description
id string auto —
variant eidos ControlVariant: 'surface' | 'outline' | 'ghost' 'surface' Forwarded to the inner <Button>. Narrowed from ButtonVariant (per DEMO_AUTHORING_GUIDE §12.7) to stay consistent with form-control surface.
size eidos 'xs' | 'sm' | 'md' | 'lg' | 'xl' 'md' Button size scale.
intent eidos 'neutral' | 'affirm' | 'fulfill' | 'risk' | 'threat' | 'loss' 'neutral' Evaluative intent forwarded to Button — full canonical Intent set.
color eidos ColorRole — see eidos/lib/types.ts — Hierarchical color override (only applies under intent='neutral').
iconOnly · icon · endIcon · rounded eidos Button props — Standard Button visual knobs forwarded through.
child {`Snippet<[{ props }]>`} — Opt out of <Button> entirely and render your own element.
…native button attrs PrimitiveButtonAttributes — All HTML button attrs flow through to the underlying button.
Content
Prop Type Default Description
forceMount boolean false Keep in DOM when closed.
side 'top' | 'right' | 'bottom' | 'left' 'bottom' Preferred side relative to the Trigger.
sideOffset number 0 Main-axis gap (pixels).
align 'start' | 'center' | 'end' 'start' Cross-axis alignment.
alignOffset number 0 Cross-axis gap.
arrowPadding number 0 Margin between Arrow and panel corner.
avoidCollisions boolean true Flip / shift when clipped.
collisionBoundary Element | Element[] viewport Boundary the floating math respects.
collisionPadding number | Partial<Record<Side, number>> 0 Inset from boundary edges.
sticky 'partial' | 'always' 'partial' Stickiness during anchor scroll.
hideWhenDetached boolean true Hide if anchor scrolls fully out of view.
updatePositionStrategy 'optimized' | 'always' 'optimized' Floating math cadence.
strategy 'fixed' | 'absolute' 'fixed' CSS positioning strategy.
loop boolean false Arrow keys wrap at boundaries.
trapFocus boolean true Tab cycles inside Content.
preventScroll boolean true Body scroll lock while open.
escapeKeydownBehavior / interactOutsideBehavior DismissalBehavior 'close' How Escape / outside-click interact with the dismissal layer.
onOpenAutoFocus / onCloseAutoFocus (e: Event) => void — Cancellable focus hooks.
onEscapeKeydown / onInteractOutside / onFocusOutside handlers — Cancellable dismissal events.
dir 'ltr' | 'rtl' inherits —
Item · CheckboxItem · RadioItem · SubTrigger
Prop Type Default Applies to Description
disabled boolean false all Skipped or just unactivatable per accessibleWhenDisabled.
textValue string — all Override for typeahead.
onSelect () => void — Item, CheckboxItem, RadioItem Activation handler.
closeOnSelect boolean true Item, CheckboxItem, RadioItem Close the menu after activation.
checked bindable boolean false CheckboxItem (standalone) Whether the checkbox is checked.
value string — CheckboxItem (in CheckboxGroup) Group identifier; item is checked when present in the group array.
onCheckedChange (checked: boolean) => void — CheckboxItem —
value (required) string — RadioItem Identifier inside the parent RadioGroup.
CheckboxGroup · RadioGroup
Prop Type Default Applies to Description
value bindable string[] [] CheckboxGroup Array of selected values (Bits UI pattern).
onValueChange (v: string[]) => void — CheckboxGroup —
value bindable string — RadioGroup Currently-selected RadioItem value.
onValueChange (v: string) => void — RadioGroup —
Sub · SubTrigger · SubContent
Prop Type Default Applies to Description
open bindable boolean false Sub Independent sub-menu open state.
side Side 'right' SubContent Default fly-out direction.
align / alignOffset / sideOffset like Content — SubContent —
avoidCollisions / collisionBoundary / collisionPadding like Content — SubContent —
loop boolean false SubContent Wrap nav inside the fly-out.
Group · GroupHeading · Separator · Arrow
Part Props Notes
Group id Wraps a block of items; aria-labelledby wires to GroupHeading.
GroupHeading id Non-interactive caption (uppercase, muted).
Separator id role='separator', 1px divider.
Arrow width, height Optional SVG pointer.
Reference parity
Feature Radix bits-ui ark-ui Eidos (this)
Button trigger ✓ ✓ ✓ ✓
CheckboxItem (single) ✓ ✓ ✓ ✓
CheckboxGroup (array bind) — ✓ — ✓
RadioGroup / RadioItem ✓ ✓ ✓ ✓
Nested Sub menus ✓ ✓ ✓ ✓
Group / GroupHeading ✓ ✓ ✓ ✓
Arrow part ✓ ✓ — ✓
closeOnSelect per item ✓ ✓ — ✓
accessibleWhenDisabled (APG) — — — ✓
commit-select semantic event — — — ✓
{/if}
{#if tab === 'morfo'}
morfo
· declarative contract
14 parts declared in the morfo + 1 eidos-only part
(CheckboxGroup wraps and binds an array — pure composition, no
new data-attrs vs Item siblings). 3 events. Source:
src/uix/morfo/components/dropdown-menu.ts.
Field Value
name "{dropdownMenuMorfo.name}"
kebab "{dropdownMenuMorfo.kebab}"
scope [{dropdownMenuMorfo.scope.map((s) => `"${s}"`).join(', ')}]
apg {dropdownMenuMorfo.apg}
parts.length {dropdownMenuMorfo.parts.length}
events.length {dropdownMenuMorfo.events?.length ?? 0}
Parts
Part Marker Element Role Archetype States Optional
{#each partsList as part (part.kebab)}
{part.kebab}
[{part.marker}]
<{part.defaultElement}>
{part.role ?? '—'}
{part.archetype ?? '—'}
{part.states.length ? part.states.join(' | ') : '—'}
{part.optional ? 'yes' : 'no'}
{/each}
{#each dropdownMenuMorfo.parts as rawPart (rawPart.kebab)}
{@const partAny = rawPart as unknown as { kebab: string; aria?: ReadonlyArray<{ attr: string; value: { kind: string }; condition?: { when: string; prop?: string; part?: string }; severity?: string }>; keyboard?: ReadonlyArray<{ key: string; action: string }> }}
{@const dataAttrs = compiled.contracts.dataAttrsByPart.get(partAny.kebab) ?? []}
{@const ariaAttrs = partAny.aria ?? []}
{@const keyboard = partAny.keyboard ?? []}
{#if dataAttrs.length || ariaAttrs.length || keyboard.length}
{partAny.kebab}
{#if dataAttrs.length}
data-attr Values Source kind
{#each dataAttrs as attr (attr.attr)}
{attr.attr}
{attr.values ? attr.values.join(' | ') : '—'}
{'value' in attr && attr.value ? (attr as { value: { kind: string } }).value.kind : '—'}
{/each}
{/if}
{#if ariaAttrs.length}
aria-attr Source kind Condition Severity
{#each ariaAttrs as a (a.attr)}
{a.attr}
{a.value.kind}
{a.condition ? `when ${a.condition.when}${a.condition.prop ? ` (${a.condition.prop})` : ''}${a.condition.part ? ` (${a.condition.part})` : ''}` : 'always'}
{a.severity ?? 'required'}
{/each}
{/if}
{#if keyboard.length}
key action
{#each keyboard as kb (kb.key)}
{kb.key === ' ' ? 'Space' : kb.key}
{kb.action}
{/each}
{/if}
{/if}
{/each}
Events declaration
open / close fire pre the state
flip; commit-select fires post with static
intent='affirm'. Play any signal from the { e.preventDefault(); tab = 'sema'; }}>Sema tab.
name family verb sequence intent target prewrite commit
{#each events as action (action.name)}
{@const sem = action.semantic}
{@const intentDecl = 'intent' in sem ? sem.intent : undefined}
{@const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}` : '—'}
{action.name}
{sem.family}
{sem.verb ?? '—'}
{sem.sequence ?? 'pre'}
{intentStr}
{action.target}
{action.prewrite.length ? action.prewrite.map((p) => `${p.attr}=${p.value}`).join(', ') : '—'}
{action.commit ? `${action.commit.attr}=${action.commit.value}` : '—'}
{/each}
{/if}
{#if tab === 'sema'}
sema
· events + perceptual signature
Three events. emerge·open / emerge·close
share the floating-overlay signature with ContextMenu, Popover and
Tooltip; commit·select with intent='affirm'
takes the affirmative-commit signature. Open the menu first so the
Content / Item targets exist before pressing play .
Name Family Verb Sequence Intent Target part Play
{#each events as action (action.name)}
{@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
{@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined}
{action.name}
{action.semantic.family}
{action.semantic.verb ?? '—'}
{action.semantic.sequence ?? 'pre'}
{effectiveIntent ?? '—'}
{action.target}
{
const targetSel =
action.target === 'item'
? '[data-dropdown-menu-item]'
: action.target === 'content'
? '[data-dropdown-menu-content]'
: '[data-dropdown-menu-trigger]';
const target = (
stageRef?.querySelector(targetSel) ??
stageRef?.querySelector('[data-dropdown-menu-trigger]') ??
stageRef
) as HTMLElement | null;
if (!target) return;
void uix.events?.emit({
name: action.name,
family: action.semantic.family,
target,
...(effectiveIntent ? { intent: effectiveIntent } : {})
});
}}
>▶ play
{/each}
{/if}
{#if tab === 'recipe'}
Eidos recipe
Source: src/uix/eidos/components/dropdown-menu/dropdown-menu.css.
Uniform chrome across Content and SubContent;
items share one set of tokens so a fly-out reads identically to
the root. All selectors target morfo-emitted markers — no
eidos-private data-attrs.
Selector Source Concern
[data-dropdown-menu-trigger] morfo Button reset + focus ring; ARIA-driven open-state hint.
[data-dropdown-menu-content], [data-dropdown-menu-sub-content] morfo Panel chrome (border, radius, surface, shadow).
[data-dropdown-menu-content][data-state='open' | 'closed'] morfo Enter / exit transitions.
[data-dropdown-menu-item], [data-dropdown-menu-checkbox-item], [data-dropdown-menu-radio-item], [data-dropdown-menu-sub-trigger] morfo Row layout, padding, gap, hover background.
[data-dropdown-menu-item][data-disabled] morfo Disabled opacity + cursor.
[data-dropdown-menu-checkbox-item][data-state='checked']::before morfo Check indicator (rounded square).
[data-dropdown-menu-radio-item][data-state='checked']::before morfo Radio indicator (circle).
[data-dropdown-menu-sub-trigger]::after eidos Right-arrow chevron pure CSS.
[data-dropdown-menu-group-heading] morfo Uppercase muted micro-caption.
[data-dropdown-menu-separator] morfo 1px divider line.
[data-dropdown-menu-arrow] morfo SVG fill matched to panel surface.
{/if}
{#if tab === 'a11y'}
Accessibility
Follows the APG menu-button pattern .
Trigger is a native <button> exposing
aria-haspopup='menu' + aria-expanded;
Content gets role='menu'.
Keyboard
Key Effect
Enter / Space on TriggerOpen the menu, focus first item.
ArrowDown on TriggerOpen the menu, focus first item.
ArrowUp / ArrowDown Move focus to previous / next item.
Home / End First / last item.
Typeahead Letters jump to the first item starting with that prefix (uses textValue when set).
Enter / Space Activate the focused item; close menu by default.
Escape Close the menu, return focus to Trigger.
ArrowRight on SubTriggerOpen sub-menu (or ArrowLeft in RTL).
ArrowLeft inside SubContentClose sub-menu, return to parent.
Tab Trapped inside Content while open.
ARIA contract
Part Attribute Value
trigger aria-haspopup "menu"
trigger aria-expanded {`{open}`}
trigger aria-controls {``}
content role "menu"
content aria-orientation "vertical"
content aria-labelledby {``}
item role "menuitem"
checkbox-item role / aria-checked "menuitemcheckbox" / {`{checked}`}
radio-item role / aria-checked "menuitemradio" / {`{checked}`}
sub-trigger aria-haspopup / aria-expanded / aria-controls "menu" / {`{open}`} / {``}
sub-content role / aria-labelledby "menu" / {``}
separator role / aria-orientation "separator" / "horizontal"
group role / aria-labelledby "group" / {``}
{/if}