Forms & inputs
Combobox
Text input with a matching option list
detent-combobox
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 3
- Wave
- phase-3-forms
- Group
- Forms & inputs
8.0 KB gzip for the class module · inlines the shared spring
Variants
No selection yet.
Recorded states
disabledrecorded state; no setteractiverecorded state; no setter
Usage
Import
import '/components/combobox/define.js';
// or register it yourself
import { DetentCombobox } from '/components/combobox/combobox.js';
Attributes
alignaria-labeldefault-valuedisabledempty-textnameplaceholderrequiredsearch-placeholdervalue
Properties
optionsvaluedefaultValueplaceholdersearchPlaceholderemptyTextaligndisabledrequirednameopenonValueChange
Slots
No slots are recorded for this component.
Events
detent-value-change— { value: string | null }detent-select— { value: string }detent-close— { reason: 'escape' | 'outside' }
Example
Runnable example: site/examples/combobox.ts. The snippet below is generated from the catalogued attributes.
<detent-combobox
align="start"
aria-label="placeholder"
default-value
disabled
empty-text="No results."
name
placeholder="Select…"
required
search-placeholder="Search…"
value
></detent-combobox>
Property-only values
const element = document.querySelector('detent-combobox');
if (element !== null) {
// options: ComboboxOption[]
element.options = [];
// onValueChange: (value: string | null) => void
element.onValueChange = () => {};
}
Source
The pinned upstream source for this row:
src/components/ui/combobox.tsxsha256 d5b37912b161…
Native files
The native module and its artifacts:
src/components/combobox/combobox.tssrc/components/combobox/styles.csssrc/components/combobox/define.tsdocs/components/combobox.mdsite/examples/combobox.tstests/browser/combobox.spec.tsevidence/components/combobox.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
align | align | 'start' | 'end' | 'start' | Kebab-case attribute; camelCase property. |
aria-label | aria-label | string | placeholder | Kebab-case attribute; camelCase property. |
default-value | defaultValue | string | null | null | Kebab-case attribute; camelCase property. |
disabled | disabled | boolean | false | Presence semantics: present means true, absent means false. |
empty-text | emptyText | string | 'No results.' | Kebab-case attribute; camelCase property. |
name | — | — | — | Recorded in the catalog as an attribute. |
placeholder | placeholder | string | 'Select…' | Kebab-case attribute; camelCase property. |
required | — | — | — | Recorded in the catalog as an attribute. |
search-placeholder | searchPlaceholder | string | 'Search…' | Kebab-case attribute; camelCase property. |
value | value | string | null | undefined | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
options | ComboboxOption[] | undefined | Property-only: assign from script; not accepted as an attribute. |
value | string | null | undefined | Property-only: assign from script; not accepted as an attribute. |
defaultValue | string | null | null | Property-only: assign from script; not accepted as an attribute. |
placeholder | string | 'Select…' | Property-only: assign from script; not accepted as an attribute. |
searchPlaceholder | string | 'Search…' | Property-only: assign from script; not accepted as an attribute. |
emptyText | string | 'No results.' | Property-only: assign from script; not accepted as an attribute. |
align | 'start' | 'end' | 'start' | Property-only: assign from script; not accepted as an attribute. |
disabled | boolean | false | Property-only: assign from script; not accepted as an attribute. |
required | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
name | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
open | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
onValueChange | (value: string | null) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-value-change | onValueChange | (value: string | null) => void | Composed, bubbles; fires only for a user action. |
detent-select | onSelect | (value: string) => void | Composed, bubbles; fires only for a user action. |
detent-close | onClose | (reason: CloseReason) => void | Composed, bubbles; fires only for a user action. |
Slots
No slots are recorded for this component.
CSS customisation
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- Controlled/uncontrolled React pair maps to initial-value attributes plus owned state and change events.
- One real text input owns `role="combobox"` (upstream had a second combobox on its panel search input - PC-12.6 correction).
- `aria-activedescendant` only; options are not focusable (no roving tabindex, never both).
- `icon` is dropped rather than inventing a slot (React node vs plain data, PC-3.1/6.3), so the row's "icons" feature is a recorded omission.
- Framer Motion popover and highlight springs become the shared spring helper with the pinned 520/40/0.6; the exit animation is dropped so aria-expanded never disagrees with the DOM.
- Form participation added: name, required, reset, default-value and state restoration.
- Upstream hex pairs and `opacity-40` disabled rows become tokens at full opacity.
- `open` is a reflected read-only state, not an input.
What it does
Typing is possible in the control itself: there is one combobox, not a button plus a second search field.
Upstream behaviour
- Single measured highlight glides to the active row and survives scrolling
- Fuzzy-free substring search across labels, descriptions, and keywords with match underline
- Descriptions, icons, and disabled options supported
- Arrow/Home/End/Enter/Esc keys wired; opens upward when there is no room below
- ARIA combobox/listbox with aria-activedescendant wired end to end
Differences from upstream
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- Controlled/uncontrolled React pair maps to initial-value attributes plus owned state and change events.
- One real text input owns `role="combobox"` (upstream had a second combobox on its panel search input - PC-12.6 correction).
- `aria-activedescendant` only; options are not focusable (no roving tabindex, never both).
- `icon` is dropped rather than inventing a slot (React node vs plain data, PC-3.1/6.3), so the row's "icons" feature is a recorded omission.
- Framer Motion popover and highlight springs become the shared spring helper with the pinned 520/40/0.6; the exit animation is dropped so aria-expanded never disagrees with the DOM.
- Form participation added: name, required, reset, default-value and state restoration.
- Upstream hex pairs and `opacity-40` disabled rows become tokens at full opacity.
- `open` is a reflected read-only state, not an input.