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

Select#

Component <neo-select> is a single-select dropdown: trigger button, popover panel, and a roving-tabindex listbox of <neo-option> / <neo-optgroup> children. The host wires keyboard navigation, type-to-search, and a lazy-by-default / async lifecycle so options can be loaded on first open and cached on subsequent opens. For search and multiple selection, see Combobox.

Default
Disabled control
Disabled options
Grouped options (time zones)
Clearable with empty slot
Rich options
Compact preview (label override)
Custom trigger face
Deactivate Rename Duplicate
Delete
New state Toggle autoplay
Today Last 7 days Last 30 days Last 3 months Last year All time
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 option value. Empty clears the selection.
namestringnoneForm field name. The control is form-associated: with a name set it submits its value in a native <form> like a <select>, resets with the form, and is barred from submission while disabled.
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.
asyncbooleanfalseOn every open, clear the listbox, re-show the [data-neo-async-placeholder] slot, and fire neo-select-load so a wired loader re-fetches. Without async, a select with a [data-neo-async-placeholder] child fires neo-select-load only on the first open (options empty) and caches across opens; neo-popover-open is independent, and always fires when the panel opens.
clearablebooleanfalseBackspace / Delete clears the selection.
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.
typeaheadtrue | falsetrueType-to-search over the options. With typeahead on, Space feeds the buffer; set typeahead="false" and Space activates the focused option instead.
typeahead-timeoutnumber500Typeahead idle reset duration in milliseconds.
typeahead-match"substring" | "prefix""substring"How the typeahead buffer matches option labels.
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 (e.g. a glyph-only trigger in a <neo-input-group>). 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]Custom skeleton shown while options load. Presence of this slot also marks the select as lazy-by-default: the kit shows it on opens with no options, then caches options across opens once they arrive.
[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-combobox>.
[data-neo-select-empty]The empty-state trigger face: its body is cloned into the trigger when nothing is selected.

Events #

NameDetailBubblesDescription
neo-select-change{ value: string | null, label: string | null }yesFires after the selection changes.
neo-select-loadnoneyesFires when options need fetching: every open with async, or a lazy select'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-select-min-width12remMinimum trigger width.

Examples#

Disabled options#

Mark an option unavailable with disabled: it renders muted, is skipped by arrow-key navigation, and can't be selected.

Standard (3–5 days) Express (1–2 days) Overnight (sold out) In-store pickup Locker drop (region unsupported) Freight (contact sales)

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.

Free Team Enterprise

Native form submission#

The select is form-associated. Give it a name and it submits its value in a plain <form>, no framework binding. Pick a plan and submit to see the posted value.

Free Team Enterprise Submit to see the posted value.

Grouped options (time zones)#

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 gives the trigger clean text even when the option body is rich markup.
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

Clearable with empty slot#

Let the user clear the selection back to an empty state.

  • clearable makes Backspace or Delete on the trigger drop the selection.
  • Tag an empty <neo-option value=""> with [data-neo-select-empty] to serve as both a selectable “none” row and the trigger face shown once cleared.
No channel SMS Email Push notification Slack DM Carrier pigeon (coming soon)

Rich options#

Options carry multi-line bodies (avatar, name, title). With no label override, the trigger clones the selected option's body, so it matches the listbox row.

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

Compact preview (label override)#

Show a compact label on the trigger while the list keeps the rich card: set each option's label and the trigger uses it instead of cloning the body.

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 custom face on the trigger, distinct from the option's full list row.

  • A [data-neo-option-trigger] child is cloned onto the trigger when its option is selected; the list keeps the full card.
  • It sits between a plain-text label and cloning the whole body, and works the same on <neo-combobox>.
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

Long list#

A long option list fits between the trigger and the viewport edge and scrolls inside the popover.

  • Strict placement (the default) keeps the panel anchored and scrolls its content.
  • Set clamp-placement to re-anchor the panel's position so it fits instead.
🇦🇫 Afghanistan🇦🇱 Albania🇩🇿 Algeria🇦🇩 Andorra🇦🇴 Angola🇦🇬 Antigua and Barbuda🇦🇷 Argentina🇦🇲 Armenia🇦🇺 Australia🇦🇹 Austria🇦🇿 Azerbaijan🇧🇸 Bahamas🇧🇭 Bahrain🇧🇩 Bangladesh🇧🇧 Barbados🇧🇾 Belarus🇧🇪 Belgium🇧🇿 Belize🇧🇯 Benin🇧🇹 Bhutan🇧🇴 Bolivia🇧🇦 Bosnia and Herzegovina🇧🇼 Botswana🇧🇷 Brazil🇧🇳 Brunei🇧🇬 Bulgaria🇧🇫 Burkina Faso🇧🇮 Burundi🇰🇭 Cambodia🇨🇲 Cameroon🇨🇦 Canada🇨🇻 Cape Verde🇨🇫 Central African Republic🇹🇩 Chad🇨🇱 Chile🇨🇳 China🇨🇴 Colombia🇰🇲 Comoros🇨🇬 Congo🇨🇷 Costa Rica🇭🇷 Croatia🇨🇺 Cuba🇨🇾 Cyprus🇨🇿 Czechia🇩🇰 Denmark🇩🇯 Djibouti🇩🇲 Dominica🇩🇴 Dominican Republic🇪🇨 Ecuador🇪🇬 Egypt🇸🇻 El Salvador🇬🇶 Equatorial Guinea🇪🇷 Eritrea🇪🇪 Estonia🇸🇿 Eswatini🇪🇹 Ethiopia🇫🇯 Fiji🇫🇮 Finland🇫🇷 France🇬🇦 Gabon🇬🇲 Gambia🇬🇪 Georgia🇩🇪 Germany🇬🇭 Ghana🇬🇷 Greece🇬🇩 Grenada🇬🇹 Guatemala🇬🇳 Guinea🇬🇼 Guinea-Bissau🇬🇾 Guyana🇭🇹 Haiti🇭🇳 Honduras🇭🇺 Hungary🇮🇸 Iceland🇮🇳 India🇮🇩 Indonesia🇮🇷 Iran🇮🇶 Iraq🇮🇪 Ireland🇮🇱 Israel🇮🇹 Italy🇯🇲 Jamaica🇯🇵 Japan🇯🇴 Jordan🇰🇿 Kazakhstan🇰🇪 Kenya🇰🇮 Kiribati🇰🇼 Kuwait🇰🇬 Kyrgyzstan🇱🇦 Laos🇱🇻 Latvia🇱🇧 Lebanon🇱🇸 Lesotho🇱🇷 Liberia🇱🇾 Libya🇱🇮 Liechtenstein🇱🇹 Lithuania🇱🇺 Luxembourg🇲🇬 Madagascar🇲🇼 Malawi🇲🇾 Malaysia🇲🇻 Maldives

Auto-flip threshold#

Strict placement flips the panel to the opposite side when its preferred side is too short.

  • If the slot is shorter than min-fit-height (default content) and the opposite side has more room, the listbox flips there.
  • Here min-fit-height is 8rem: when the trigger sits low in the viewport the listbox flips from bottom-start to top-start. Scroll it down to see it.
🇦🇫 Afghanistan🇦🇱 Albania🇩🇿 Algeria🇦🇩 Andorra🇦🇴 Angola🇦🇬 Antigua and Barbuda🇦🇷 Argentina🇦🇲 Armenia🇦🇺 Australia🇦🇹 Austria🇦🇿 Azerbaijan🇧🇸 Bahamas🇧🇭 Bahrain🇧🇩 Bangladesh🇧🇧 Barbados🇧🇾 Belarus🇧🇪 Belgium🇧🇿 Belize🇧🇯 Benin🇧🇹 Bhutan🇧🇴 Bolivia🇧🇦 Bosnia and Herzegovina🇧🇼 Botswana🇧🇷 Brazil🇧🇳 Brunei🇧🇬 Bulgaria🇧🇫 Burkina Faso🇧🇮 Burundi🇰🇭 Cambodia🇨🇲 Cameroon🇨🇦 Canada🇨🇻 Cape Verde🇨🇫 Central African Republic🇹🇩 Chad🇨🇱 Chile🇨🇳 China🇨🇴 Colombia🇰🇲 Comoros🇨🇬 Congo🇨🇷 Costa Rica🇭🇷 Croatia🇨🇺 Cuba🇨🇾 Cyprus🇨🇿 Czechia🇩🇰 Denmark🇩🇯 Djibouti🇩🇲 Dominica🇩🇴 Dominican Republic🇪🇨 Ecuador🇪🇬 Egypt🇸🇻 El Salvador🇬🇶 Equatorial Guinea🇪🇷 Eritrea🇪🇪 Estonia🇸🇿 Eswatini🇪🇹 Ethiopia🇫🇯 Fiji🇫🇮 Finland🇫🇷 France🇬🇦 Gabon🇬🇲 Gambia🇬🇪 Georgia🇩🇪 Germany🇬🇭 Ghana🇬🇷 Greece🇬🇩 Grenada🇬🇹 Guatemala🇬🇳 Guinea🇬🇼 Guinea-Bissau🇬🇾 Guyana🇭🇹 Haiti🇭🇳 Honduras🇭🇺 Hungary🇮🇸 Iceland🇮🇳 India🇮🇩 Indonesia🇮🇷 Iran🇮🇶 Iraq🇮🇪 Ireland🇮🇱 Israel🇮🇹 Italy🇯🇲 Jamaica🇯🇵 Japan🇯🇴 Jordan🇰🇿 Kazakhstan🇰🇪 Kenya🇰🇮 Kiribati🇰🇼 Kuwait🇰🇬 Kyrgyzstan🇱🇦 Laos🇱🇻 Latvia🇱🇧 Lebanon🇱🇸 Lesotho🇱🇷 Liberia🇱🇾 Libya🇱🇮 Liechtenstein🇱🇹 Lithuania🇱🇺 Luxembourg🇲🇬 Madagascar🇲🇼 Malawi🇲🇾 Malaysia🇲🇻 Maldives

Examples: Datastar#

Morphing during interaction#

A Datastar fat-morph can replace the option list while the user is interacting; the select 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
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-select-load; wire data-on:neo-select-load to post.
  • The server morphs an options fragment into #<host-id>-options; the [data-neo-async-placeholder] shows until it arrives.
  • With clearable, Backspace clears the selection and the next open reloads.
  • Flip server unreachable or a 4xx/5xx server response status to see the placeholder stay put when the request never lands.

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-select-load.
  • Loaded options are cached; later opens reuse them with no refetch. The option state is the only source of truth, with no guard signal needed.
  • A server reset that re-emits the placeholder puts the select back into the "needs load" state.

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

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.

Settings

Theme

Default Sunshine Ocean Matrix Rose

Accessibility

Playgrounds

Server Simulator