A wider screen, please

This page features interactive demos and side-by-side code samples that need at least 340 pixels of horizontal space. Please widen your browser window, or rotate your device, to continue.

Morpheus v0.1.0
v0.1.0

Combobox#

Component <neo-combobox> is a search-driven sibling of Select. Trigger, popover, <neo-option> / <neo-optgroup> children, and the lazy / async lifecycle are identical; the panel adds a pinned, auto-focusing search input that filters the listbox client-side. ArrowDown / ArrowUp bridge between input and listbox at either end.

Default
Disabled control
Grouped options
Multiple selection
Rich options
Custom trigger face
Multi-select chips
Open above
Deactivate Rename Duplicate
Delete
New state Toggle autoplay
Amsterdam Berlin Copenhagen Dublin Edinburgh Florence Geneva Helsinki Istanbul Lisbon London Madrid Oslo Paris Prague Rome Stockholm Vienna Warsaw Zürich
No cities match "".
HTML CSS

The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.

Replace Patch

Signals

Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.

Patch

Reference#

Attributes #

NameTypeDefaultDescription
valuestring reflected observednoneSelected value(s); comma-separated when multiple is set. In multiple mode an option value must not contain a comma; the separator is unescaped.
namestring observednoneForm field name. The control is form-associated: with a name set it submits its value in a native <form>, resets with the form, and is barred from submission while disabled. With multiple it submits one entry per selected value under the same name, like <select multiple>.
size"sm" | "lg"noneControl-size step: sm compact, lg large. Omit for the default size.
disabledboolean observedfalseMakes the control non-interactive: the trigger drops out of the tab order and ignores clicks and keys, and an open panel closes.
placeholderstring observednoneTrigger text when nothing is selected.
liststring observednoneReferences a shared <neo-datalist> by its id (like native <input list>) to source the options. Inline <neo-option> children (or an inline datalist) take precedence.
search-placeholderstring observed"Search…"Hint shown in the search input.
multipleboolean observedfalseAllow multi-selection; value becomes a comma-separated list.
clearablebooleanfalseBackspace / Delete on the focused (closed) trigger clears the selection back to the empty state.
caretstring observed"chevrons-up-down"Name of the trigger's caret icon. Resolved through <neo-icon>, so it follows the active icon theme. Set caret="" (empty) to hide the caret entirely.
asyncbooleanfalseOn every open, clear the listbox, re-show the [data-neo-async-placeholder] slot, and fire neo-combobox-load so a wired loader re-fetches. Without async, a combobox with a [data-neo-async-placeholder] child fires neo-combobox-load only on the first open (options empty) and caches across opens; neo-popover-open is independent; it always fires when the panel opens.
live-searchbooleanfalseReload options on every keystroke.
search-debouncenumber0Milliseconds to coalesce keystrokes before running the local filter / live-search loading reset and dispatching neo-combobox-search. 0 dispatches on every keystroke.
placementstring observed"bottom-start"Anchor placement of the panel. The listbox positions the panel with the same helper as <neo-popover>; see its placement values.
screen-offsetstring observednoneViewport edge margin and the panel's scrollable max size.
follow-scroll"always" | "until-trigger-invisible" observedunsetWhen absent, close on outside scroll. Set to always to stay anchored during scroll. Set to until-trigger-invisible to close once the trigger leaves the viewport.
clamp-placementboolean observedfalseDefault is strict placement (panel anchored at the exact point, content scrolls); set this to opt into re-anchoring the panel position so it stays inside the viewport when content would overflow.
min-fit-heightCSS length | "content" observed"content"Minimum vertical slot the strict placement requires; below the threshold the panel auto-flips to the opposite primary axis when the other side has more room. content resolves from the panel's scroll height on each positioning pass.
min-fit-widthCSS length | "content" observed"content"Same as min-fit-height but for the horizontal axis. content resolves from the panel's scroll width. Use 0 to disable the horizontal auto-flip.
min-open-heightCSS length observed2remMinimum usable vertical slot required to remain open after placement and auto-flip. If the bounded slot is smaller, the listbox closes instead of rendering a cramped panel. Use 0 to disable.
min-open-widthCSS length observed2remSame as min-open-height but for horizontal space. Use 0 to disable.
popover-fit-contentbooleanfalseThe listbox tracks the trigger width by default; set this to size the panel to the option rows instead. Capped by --neo-popover-max-width and the viewport.
hoverbooleanfalseOpen the panel on trigger hover without moving focus; close after the pointer leaves both trigger and panel. Mouse only; touch falls back to click. While set, clicking the trigger always shows the panel instantly and never toggles it shut, so touch and keyboard users keep a way in.
hover-open-delayinteger (ms)100Milliseconds the pointer must rest on the trigger before hover opens the panel. Filters incidental cursor crossings. Does not apply to click.
hover-close-delayinteger (ms)200Milliseconds to wait after the pointer leaves both trigger and panel before hover closes the panel. Long enough to bridge the gap between them.

Slots #

NameDescription
default<neo-option> and <neo-optgroup> children. Alternatively, set list to source them from a shared <neo-datalist>.
[data-neo-async-placeholder]Shown in the listbox whenever there are no options. The kit clones the slot under the search field; when the slot is absent, a default <neo-spinner> stands in.
[slot=trigger]Owns the trigger face. Leave it absent and the kit fills it client-side by cloning the selected option's body (so its CSS styles it and the trigger grows to fit); provide one (e.g. patched from a server) and the kit yields, rendering exactly your content.
[data-neo-option-trigger]Optional per-<neo-option> child: a compact trigger face distinct from the option's rich list row. Hidden in the list; cloned into the trigger when that option is selected. Falls back to the option's label (plain text), then its full body. Shared with <neo-select>.
[data-neo-combobox-empty]The empty-state trigger face, cloned into the trigger when nothing is selected. Hidden in the list.
[data-neo-empty-results]Overrides the default No results hint when the listbox ends up empty after filtering or after an external load returned no options. Any [data-neo-empty-query] descendants get their textContent set to the current search input each time the slot is mounted, so authors can render messages like No results for "<span data-neo-empty-query></span>". A response can also ship one inline as an option-list child.

Events #

NameDetailBubblesDescription
neo-combobox-change{ value, label } or { values, labels }yesFires after the selection changes. Multiple-mode payload uses array fields.
neo-combobox-search{ query: string }yesFires on every search-input change; detail.query carries the current text. For server-driven live search, copy it into a signal in the handler (the element binds no framework attribute itself).
neo-combobox-loadnoneyesFires when options need fetching: every open with async, or a lazy combobox's first open. Wire a loader to it (e.g. a Datastar @post). Call el.reload() to re-fire it. Distinct from neo-popover-open, which always fires on open.

Parts #

NameDescription
::part(label)Trigger label showing the current selection.
::part(caret)Trigger caret icon.

CSS variables #

NameDefaultDescription
--neo-combobox-min-widthvar(--neo-select-min-width, 12rem)Minimum trigger width.
--neo-combobox-search-paddingcalc(var(--page-spacing, 0.25rem) * 2) calc(var(--page-spacing, 0.25rem) * 2.5)Padding around the search field.
--neo-combobox-search-dividercolor-mix(in srgb, var(--page-fg) 10%, transparent)Divider color between the search field and the options list.
--neo-combobox-search-divider-width1pxDivider width between the search field and the options list.

Examples#

Disabled control#

Set disabled on the host to make the whole control non-interactive: the trigger leaves the tab order, ignores clicks and keys, and renders faded. An open panel closes. Distinct from disabling individual options.

Sydney Frankfurt Tokyo

Native form submission#

The combobox is form-associated. Give it a name and it submits in a plain <form>, no framework binding. In multiple mode it posts one entry per selected value under the same name, like <select multiple>. Submit to see the posted values.

Sydney Frankfurt Tokyo Oregon Submit to see the posted values.

Grouped options#

Group related options under labeled headers by wrapping them in <neo-optgroup label="…">.

  • The group header is not focusable; arrow keys step across groups.
  • Each option's label keeps the trigger and search clean text, even when the option body is rich markup.
  • Searching hides a whole group once all its options drop out. Type tokyo to watch the others collapse.
Los AngelesUTC−08:00 New YorkUTC−05:00 São PauloUTC−03:00 LondonUTC+00:00 BerlinUTC+01:00 CairoUTC+02:00 DubaiUTC+04:00 TokyoUTC+09:00 SydneyUTC+11:00

Multiple selection#

Pick several options at once with multiple.

  • Clicking an option toggles it and keeps the popover open; single-select replaces and closes.
  • value is a comma-separated list and neo-combobox-change carries {values, labels}.
  • A [data-neo-empty-results] child replaces the no-match row; [data-neo-empty-query] inside it echoes the typed text. Type xyz to see it.
C C++ Rust Zig Go Java Kotlin Swift Elixir Haskell OCaml
No languages match "@neo.EmptyQuery()".

Rich options#

Options carry rich multi-line bodies (avatar, name, title) that also render in the trigger.

  • The selected option's body is cloned into the trigger, which grows to fit it.
  • Search matches each option's label or text.
  • [data-neo-combobox-empty] is the face shown when nothing is selected; clearable lets Backspace or Delete on the trigger clear the selection.
  • A server can swap the trigger face by patching a slot="trigger" child.
Unassigned
No owner picked yet
Diego Vega
CFO
Jan O'Sullivan
Lead Designer
Evelyn Kone
Data Analyst
Samira Khalil
Head of Engineering
Alice Larsson
Product Manager
Camila Duarte
DevOps Engineer
Theo Becker
UX Researcher
Mateusz Wiśniewski
Fullstack Engineer

Custom trigger face#

Show a compact face on the trigger, separate from the full row in the list.

  • A [data-neo-option-trigger] child is cloned onto the trigger; the rest of the option stays the list row.
  • [data-neo-combobox-empty] is the empty face. Press Backspace on the trigger to clear and show it.
Unassigned
Diego Vega
Diego Vega
CFO
Jan O'Sullivan
Jan O'Sullivan
Lead Designer
Evelyn Kone
Evelyn Kone
Data Analyst
Samira Khalil
Samira Khalil
Head of Engineering
Alice Larsson
Alice Larsson
Product Manager
Camila Duarte
Camila Duarte
DevOps Engineer
Theo Becker
Theo Becker
UX Researcher
Mateusz Wiśniewski
Mateusz Wiśniewski
Fullstack Engineer

Multi-select chips#

multiple with per-option faces: each pick becomes a chip on the trigger.

  • A chip uses the option's [data-neo-option-trigger] face, or falls back to its text.
  • Chips wrap and the trigger grows. Drag the stage narrower to watch them reflow.
Diego Vega
Diego Vega
CFO
Jan O'Sullivan
Jan O'Sullivan
Lead Designer
Evelyn Kone
Evelyn Kone
Data Analyst
Samira Khalil
Samira Khalil
Head of Engineering
Alice Larsson
Alice Larsson
Product Manager
Camila Duarte
Camila Duarte
DevOps Engineer
Theo Becker
Theo Becker
UX Researcher
Mateusz Wiśniewski
Mateusz Wiśniewski
Fullstack Engineer

Open above#

Open the panel above the trigger with placement="top-start".

  • The search input stays pinned next to the trigger (bottom of the panel), even as placement flips.
  • ArrowUp moves to the nearest option (last in the DOM); ArrowDown wraps to the first.
London New York Tokyo

Examples: Datastar#

Morphing during interaction#

A Datastar fat-morph can replace the option list while the user is interacting; the combobox reconciles in place.

  • Options can appear, disappear, or flip to disabled with the popover open.
  • The element stays mounted, the popover stays open, and the selection survives. Autoplay cycles the states below to simulate the morphs.
Default
Add more options
Disable some options
Deactivate Rename Duplicate
Delete
New state Toggle autoplay
Amsterdam Berlin Copenhagen Dublin Edinburgh
No cities match "".
HTML CSS

The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.

Replace Patch

Signals

Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.

Patch

Async load#

Fetch options from the server on every open with async.

  • Each open fires neo-combobox-load; wire data-on:neo-combobox-load to post.
  • The server morphs an options fragment into #<host-id>-options.
  • [data-neo-async-placeholder] fills the listbox until options arrive; the search field stays usable.

Loading states only show with simulated delay. Raise network latency or handler delay in the Settings panel.

Loading…

Lazy load#

Fetch options once, on first open, then reuse them.

  • Add a [data-neo-async-placeholder] without async: the first open fires neo-combobox-load.
  • Loaded options are cached; later opens reuse them with no refetch.

Loading states only show with simulated delay. Raise network latency or handler delay in the Settings panel.

Loading…

Live search (server-driven)#

Let the server filter on every keystroke instead of matching locally, with live-search.

  • Each keystroke re-shows the placeholder and refetches; wire data-on:neo-combobox-search to re-post.
  • The event's event.detail.query holds the typed text; copy it into a signal the server reads.
  • The server returns the matching options, or a [data-neo-empty-results] child when nothing matches.
Loading…

Async load with failure swap#

Async load that shows a failure state and a retry button when the request keeps failing.

  • On a failed request Datastar retries per its retry config; after retries are exhausted, a failure template replaces the [data-neo-async-placeholder].
  • A [data-neo-popover-async-retry] button re-runs the load without closing the panel.
  • The placeholder stays the source, so reopening shows the loading view again.

Loading states only show with simulated delay. Raise network latency or handler delay in the Settings panel.

Loading…

Settings

Theme

Default Sunshine Ocean Matrix Rose

Accessibility

Playgrounds

Server Simulator