Forms · Combobox

Combobox

Editable input with a floating listbox. Soma owns input text, open state, selection, keyboard navigation, groups and form values; Eidos owns the control layout, trigger icon, selected item indicator and panel recipe.

parts{partsList.length} events{events.length} mode{mode} scope{comboboxMorfo.scope.join(' · ')}
{#each groups as group} {@const groupOptions = filteredOptions.filter((option) => option.group === group)} {#if groupOptions.length} {group} {#each groupOptions as option} {option.label} {/each} {/if} {/each}
trace {#if trace.length === 0} type and select an item to see Sema events {:else} {#each trace.slice(0, 3) as entry} {entry.event} · {entry.family}{entry.intent ? ' · ' + entry.intent : ''} {fmtTime(entry.at)} {/each} {/if} value {selectedLabels.length ? selectedLabels.join(', ') : 'none'}
{#if tab === 'live'}

Controls

Controls are grouped by owner. The demo filters rendered items from inputValue; Soma owns the input state, but the consumer owns its collection query.

soma props · headless behavior
eidos props · visual treatment
soma headless · input + listbox behavior svelte
{somaSnippet}
eidos visual · control/content recipe svelte
{eidosSnippet}
{/if} {#if tab === 'api'}

API + reference comparison

FeatureAirRadixBitsArkReact AriaUIXDecision
Editable input + listbox✅❌✅✅✅✅Covered.
Control wrapper✅❌✅✅✅✅Eidos-only visual layout.
Portal + floating + arrow✅❌✅✅✅✅Soma owns floating.
Groups + separator✅❌✅✅✅✅Covered.
Multiple select✅❌✅✅❌✅Soma type="multiple".
Clear trigger❌❌⚠️✅✅❌Defer to Soma.
Scroll buttons✅❌✅⚠️❌❌Defer to Soma.
Menu trigger behavior❌❌⚠️✅✅❌Defer to Soma.
Validation / async collections✅❌⚠️✅✅❌Defer to Field/Soma/collections.
PropOwnerDefaultNotes
valuesoma[]Bindable selected values.
inputValuesoma''Bindable text query; consumer filters items.
opensomafalseBindable popup state.
typesomasinglesingle | multiple.
allowDeselectsomafalseSingle mode can clear by reselecting.
side / alignsomabottom / startFloating placement. No auto side.
sizeeidosmdResponsive visual density.
varianteidossurfacesurface | outline | ghost.
coloreidosprimaryIntent palette for focus/selected state.
{/if} {#if tab === 'morfo'}

morfo · contract

Source: src/uix/morfo/components/combobox.ts. Morfo is the source for markers, ARIA, keyboard and semantic event targets.

FieldValue
name"{comboboxMorfo.name}"
kebab"{comboboxMorfo.kebab}"
scope[{comboboxMorfo.scope.map((s) => `"${s}"`).join(', ')}]
apg{comboboxMorfo.apg}
parts.length{comboboxMorfo.parts.length}
events.length{comboboxMorfo.events.length}
Parts
{#each partsList as part} {/each}
PartMarkerElementRoleArchetypeStatesOptional
{part.kebab} [{part.marker}] <{part.defaultElement}> {part.role ?? '-'} {part.archetype ?? '-'} {part.states.length ? part.states.join(' | ') : '-'} {part.optional ? 'yes' : 'no'}
{#each comboboxMorfo.parts as rawPart} {@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} {/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} {/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} {/each}
keyaction
{kb.key === ' ' ? 'Space' : kb.key} {kb.action}
{/if} {/if} {/each}
Events declaration
{#each events as action} {@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

Combobox has two semantic commits: selecting is affirmative; unselecting is neutral. Typing, filtering, opening and highlighting are intentionally silent.

{#each events as action} {/each}
NameFamilyVerbSequenceIntentTargetPlay
{action.name} {action.semantic.family} {action.semantic.verb ?? '-'} {action.semantic.sequence ?? 'pre'} {action.semantic.intent ?? '-'} {action.target}
{/if} {#if tab === 'recipe'}

Eidos recipe

Selectors live in src/uix/eidos/components/combobox/combobox.css. Morfo provides structure/state; Eidos-only attrs provide visual treatment.

SelectorSource
[data-combobox]morfo
[data-combobox-control]eidos-only
[data-combobox-input][data-state]morfo
[data-combobox-content][data-side]runtime
[data-combobox-item][data-state='checked']morfo
[data-combobox-input][data-size]eidos
[data-combobox-indicator]eidos-only
[data-combobox-item-indicator]eidos-only
{/if} {#if tab === 'a11y'}

Accessibility

ConcernContract
PatternInput is role="combobox"; content is role="listbox"; items are role="option".
KeyboardArrows navigate, Home/End jump, Enter selects, Escape closes.
FocusTyping keeps focus in the input; highlighted item id is exposed via aria-activedescendant.
Formname renders hidden inputs for submission.
DisabledDisabled input, trigger and items are removed from interaction.
{/if}