Detent web components
By Shane Logsdon

Forms & inputs

Combobox

Implemented

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

seeded
align end
disabled

No selection yet.

Recorded states

  • disabled recorded state; no setter
  • active recorded state; no setter

Usage

Import

import '/components/combobox/define.js';

// or register it yourself
import { DetentCombobox } from '/components/combobox/combobox.js';

Attributes

  • align
  • aria-label
  • default-value
  • disabled
  • empty-text
  • name
  • placeholder
  • required
  • search-placeholder
  • value

Properties

  • options
  • value
  • defaultValue
  • placeholder
  • searchPlaceholder
  • emptyText
  • align
  • disabled
  • required
  • name
  • open
  • onValueChange

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.tsx sha256 d5b37912b161…

Native files

The native module and its artifacts:

  • src/components/combobox/combobox.ts
  • src/components/combobox/styles.css
  • src/components/combobox/define.ts
  • docs/components/combobox.md
  • site/examples/combobox.ts
  • tests/browser/combobox.spec.ts
  • evidence/components/combobox.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
alignalign'start' | 'end''start'Kebab-case attribute; camelCase property.
aria-labelaria-labelstringplaceholderKebab-case attribute; camelCase property.
default-valuedefaultValuestring | nullnullKebab-case attribute; camelCase property.
disableddisabledbooleanfalsePresence semantics: present means true, absent means false.
empty-textemptyTextstring'No results.'Kebab-case attribute; camelCase property.
name———Recorded in the catalog as an attribute.
placeholderplaceholderstring'Select…'Kebab-case attribute; camelCase property.
required———Recorded in the catalog as an attribute.
search-placeholdersearchPlaceholderstring'Search…'Kebab-case attribute; camelCase property.
valuevaluestring | nullundefinedKebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
optionsComboboxOption[]undefinedProperty-only: assign from script; not accepted as an attribute.
valuestring | nullundefinedProperty-only: assign from script; not accepted as an attribute.
defaultValuestring | nullnullProperty-only: assign from script; not accepted as an attribute.
placeholderstring'Select…'Property-only: assign from script; not accepted as an attribute.
searchPlaceholderstring'Search…'Property-only: assign from script; not accepted as an attribute.
emptyTextstring'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.
disabledbooleanfalseProperty-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) => voidundefinedProperty-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-value-changeonValueChange(value: string | null) => voidComposed, bubbles; fires only for a user action.
detent-selectonSelect(value: string) => voidComposed, bubbles; fires only for a user action.
detent-closeonClose(reason: CloseReason) => voidComposed, 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.
Previous: Code Snippet Deck Next: Command Menu

Docs

Components

Project