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}
{#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
soma Content props · floating layer · positioning + focus mgmt
soma Item props · applied to the Log out item below
Composition · toggle parts in / out of the markup
eidos Trigger · visual props forwarded to the underlying <Button>
Bound state · `bind:checked` / `bind:value` mirrors from the live menu items
Demo content · literal labels rendered into the menu
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)
PropTypeDefaultDescription
idstringautoStable identifier.
open bindablebooleanfalseWhether 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.
accessibleWhenDisabledbooleantrueDisabled items remain focusable per WAI-ARIA APG; set false to skip them entirely.
Trigger
PropTypeDefaultDescription
idstringauto—
variant eidosControlVariant: '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 eidosColorRole — see eidos/lib/types.ts—Hierarchical color override (only applies under intent='neutral').
iconOnly · icon · endIcon · rounded eidosButton props—Standard Button visual knobs forwarded through.
child{`Snippet<[{ props }]>`}—Opt out of <Button> entirely and render your own element.
…native button attrsPrimitiveButtonAttributes—All HTML button attrs flow through to the underlying button.
Content
PropTypeDefaultDescription
forceMountbooleanfalseKeep in DOM when closed.
side'top' | 'right' | 'bottom' | 'left''bottom'Preferred side relative to the Trigger.
sideOffsetnumber0Main-axis gap (pixels).
align'start' | 'center' | 'end''start'Cross-axis alignment.
alignOffsetnumber0Cross-axis gap.
arrowPaddingnumber0Margin between Arrow and panel corner.
avoidCollisionsbooleantrueFlip / shift when clipped.
collisionBoundaryElement | Element[]viewportBoundary the floating math respects.
collisionPaddingnumber | Partial<Record<Side, number>>0Inset from boundary edges.
sticky'partial' | 'always''partial'Stickiness during anchor scroll.
hideWhenDetachedbooleantrueHide if anchor scrolls fully out of view.
updatePositionStrategy'optimized' | 'always''optimized'Floating math cadence.
strategy'fixed' | 'absolute''fixed'CSS positioning strategy.
loopbooleanfalseArrow keys wrap at boundaries.
trapFocusbooleantrueTab cycles inside Content.
preventScrollbooleantrueBody scroll lock while open.
escapeKeydownBehavior / interactOutsideBehaviorDismissalBehavior'close'How Escape / outside-click interact with the dismissal layer.
onOpenAutoFocus / onCloseAutoFocus(e: Event) => void—Cancellable focus hooks.
onEscapeKeydown / onInteractOutside / onFocusOutsidehandlers—Cancellable dismissal events.
dir'ltr' | 'rtl'inherits—
Item · CheckboxItem · RadioItem · SubTrigger
PropTypeDefaultApplies toDescription
disabledbooleanfalseallSkipped or just unactivatable per accessibleWhenDisabled.
textValuestring—allOverride for typeahead.
onSelect() => void—Item, CheckboxItem, RadioItemActivation handler.
closeOnSelectbooleantrueItem, CheckboxItem, RadioItemClose the menu after activation.
checked bindablebooleanfalseCheckboxItem (standalone)Whether the checkbox is checked.
valuestring—CheckboxItem (in CheckboxGroup)Group identifier; item is checked when present in the group array.
onCheckedChange(checked: boolean) => void—CheckboxItem—
value (required)string—RadioItemIdentifier inside the parent RadioGroup.
CheckboxGroup · RadioGroup
PropTypeDefaultApplies toDescription
value bindablestring[][]CheckboxGroupArray of selected values (Bits UI pattern).
onValueChange(v: string[]) => void—CheckboxGroup—
value bindablestring—RadioGroupCurrently-selected RadioItem value.
onValueChange(v: string) => void—RadioGroup—
Sub · SubTrigger · SubContent
PropTypeDefaultApplies toDescription
open bindablebooleanfalseSubIndependent sub-menu open state.
sideSide'right'SubContentDefault fly-out direction.
align / alignOffset / sideOffsetlike Content—SubContent—
avoidCollisions / collisionBoundary / collisionPaddinglike Content—SubContent—
loopbooleanfalseSubContentWrap nav inside the fly-out.
Group · GroupHeading · Separator · Arrow
PartPropsNotes
GroupidWraps a block of items; aria-labelledby wires to GroupHeading.
GroupHeadingidNon-interactive caption (uppercase, muted).
Separatoridrole='separator', 1px divider.
Arrowwidth, heightOptional SVG pointer.
Reference parity
FeatureRadixbits-uiark-uiEidos (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.

FieldValue
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
{#each partsList as part (part.kebab)} {/each}
PartMarkerElementRoleArchetypeStatesOptional
{part.kebab} [{part.marker}] <{part.defaultElement}> {part.role ?? '—'} {part.archetype ?? '—'} {part.states.length ? part.states.join(' | ') : '—'} {part.optional ? 'yes' : 'no'}
{#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}
{#each dataAttrs as attr (attr.attr)} {/each}
data-attrValuesSource kind
{attr.attr} {attr.values ? attr.values.join(' | ') : '—'} {'value' in attr && attr.value ? (attr as { value: { kind: string } }).value.kind : '—'}
{/if} {#if ariaAttrs.length}
{#each ariaAttrs as a (a.attr)} {/each}
aria-attrSource kindConditionSeverity
{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'}
{/if} {#if keyboard.length}
{#each keyboard as kb (kb.key)} {/each}
keyaction
{kb.key === ' ' ? 'Space' : kb.key} {kb.action}
{/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.

{#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 ?? '?'}` : '—'} {/each}
namefamilyverbsequenceintenttargetprewritecommit
{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}` : '—'}
{/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.

{#each events as action (action.name)} {@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined} {@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined} {/each}
NameFamilyVerbSequenceIntentTarget partPlay
{action.name} {action.semantic.family} {action.semantic.verb ?? '—'} {action.semantic.sequence ?? 'pre'} {effectiveIntent ?? '—'} {action.target}
{/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.

SelectorSourceConcern
[data-dropdown-menu-trigger]morfoButton reset + focus ring; ARIA-driven open-state hint.
[data-dropdown-menu-content], [data-dropdown-menu-sub-content]morfoPanel chrome (border, radius, surface, shadow).
[data-dropdown-menu-content][data-state='open' | 'closed']morfoEnter / exit transitions.
[data-dropdown-menu-item], [data-dropdown-menu-checkbox-item], [data-dropdown-menu-radio-item], [data-dropdown-menu-sub-trigger]morfoRow layout, padding, gap, hover background.
[data-dropdown-menu-item][data-disabled]morfoDisabled opacity + cursor.
[data-dropdown-menu-checkbox-item][data-state='checked']::beforemorfoCheck indicator (rounded square).
[data-dropdown-menu-radio-item][data-state='checked']::beforemorfoRadio indicator (circle).
[data-dropdown-menu-sub-trigger]::aftereidosRight-arrow chevron pure CSS.
[data-dropdown-menu-group-heading]morfoUppercase muted micro-caption.
[data-dropdown-menu-separator]morfo1px divider line.
[data-dropdown-menu-arrow]morfoSVG 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
KeyEffect
Enter / Space on TriggerOpen the menu, focus first item.
ArrowDown on TriggerOpen the menu, focus first item.
ArrowUp / ArrowDownMove focus to previous / next item.
Home / EndFirst / last item.
TypeaheadLetters jump to the first item starting with that prefix (uses textValue when set).
Enter / SpaceActivate the focused item; close menu by default.
EscapeClose the menu, return focus to Trigger.
ArrowRight on SubTriggerOpen sub-menu (or ArrowLeft in RTL).
ArrowLeft inside SubContentClose sub-menu, return to parent.
TabTrapped inside Content while open.
ARIA contract
PartAttributeValue
triggeraria-haspopup"menu"
triggeraria-expanded{`{open}`}
triggeraria-controls{``}
contentrole"menu"
contentaria-orientation"vertical"
contentaria-labelledby{``}
itemrole"menuitem"
checkbox-itemrole / aria-checked"menuitemcheckbox" / {`{checked}`}
radio-itemrole / aria-checked"menuitemradio" / {`{checked}`}
sub-triggeraria-haspopup / aria-expanded / aria-controls"menu" / {`{open}`} / {``}
sub-contentrole / aria-labelledby"menu" / {``}
separatorrole / aria-orientation"separator" / "horizontal"
grouprole / aria-labelledby"group" / {``}
{/if}