Detent web components
By Shane Logsdon

Forms & inputs

SpringSelect

Implemented

Select menu that follows its trigger

detent-spring-select

Port status
Catalog status
implemented
Origin
upstream
Phase
3
Wave
phase-3-forms
Group
Forms & inputs

7.4 KB gzip for the class module · inlines the shared spring

Variants

No selection yet.

Recorded states

  • hover recorded state; no setter
  • disabled recorded state; no setter
  • closed recorded state; no setter
  • expanded recorded state; no setter
  • selected recorded state; no setter
  • dark theme recorded state; no setter
  • active recorded state; no setter

Usage

Import

import '/components/spring-select/define.js';

// or register it yourself
import { DetentSpringSelect } from '/components/spring-select/spring-select.js';

Attributes

  • default-value
  • disabled
  • error
  • label
  • name
  • placeholder
  • required
  • value

Properties

  • options
  • value
  • defaultValue
  • placeholder
  • label
  • error
  • disabled
  • required
  • name
  • open
  • onChange

Slots

No slots are recorded for this component.

Events

  • detent-change — { value: string }

Example

Runnable example: site/examples/spring-select.ts. The snippet below is generated from the catalogued attributes.

<detent-spring-select
  default-value
  disabled
  error
  label
  name
  placeholder="Select…"
  required
  value
></detent-spring-select>

Property-only values

const element = document.querySelector('detent-spring-select');
if (element !== null) {
  // options: SpringSelectOption[]
  element.options = [];
  // onChange: (value: string) => void
  element.onChange = () => {};
}

Source

The pinned upstream source for this row:

  • src/components/ui/SpringSelect.tsx sha256 9cf1a4d482c2…

Native files

The native module and its artifacts:

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

API

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

Attributes

Attributes
AttributePropertyTypeDefaultNotes
default-valuedefaultValuestringundefinedKebab-case attribute; camelCase property.
disableddisabledbooleanfalsePresence semantics: present means true, absent means false.
errorerrorstringundefinedKebab-case attribute; camelCase property.
labellabelstringundefinedKebab-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.
valuevaluestringundefinedKebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
optionsSpringSelectOption[][]Property-only: assign from script; not accepted as an attribute.
valuestringundefinedProperty-only: assign from script; not accepted as an attribute.
defaultValuestringundefinedProperty-only: assign from script; not accepted as an attribute.
placeholderstring'Select…'Property-only: assign from script; not accepted as an attribute.
labelstringundefinedProperty-only: assign from script; not accepted as an attribute.
errorstringundefinedProperty-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.
onChange(value: string) => voidundefinedProperty-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-changeonChange(value: string) => voidComposed, bubbles; fires only for a user action.

Slots

No slots are recorded for this component.

CSS customisation

  • 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.
  • Roving DOM focus on the options replaces the upstream activeIndex-only highlight (one focus mechanism, PC-12.6).
  • Native `<select>` type-ahead added (printable keys with a 600ms buffer) so a select stays typeable; no attribute or event added.
  • Framer Motion panel, chevron and check springs become the shared spring helper with the pinned springSnappy 380/30/0.5; the AnimatePresence exit is dropped.
  • Form participation added: name, required, reset, default-value and state restoration.
  • Upstream --surface/--border/rose/opacity-30 treatment becomes tokens at full opacity.
  • `error` is presentational (role=alert, aria-invalid, aria-describedby) and does not replace native constraint validation.
  • The row says select, so it is a real button with aria-haspopup=listbox plus role=listbox/option: no menu roles, no text input, no aria-activedescendant.

What it does

A select-only combobox: a real button trigger, a role="listbox" panel that springs in with a 0.98 → 1 scale overshoot, a chevron that rotates past 180° and settles, and a check mark that follows the selection.

Upstream behaviour
  • Dropdown panel springs from the trigger with a subtle scale: 0.98 -> 1 overshoot
  • Chevron rotates to 192° (slightly past 180°) and settles back via snappy spring
  • Keyboard navigation: ArrowUp/Down to move, Enter to select, Escape to close
  • Hover and active item highlight via tracked activeIndex state
  • Selected item checkmark that follows selection through the spring motion
  • Light/dark theme aware via CSS variables — works in both palettes
Differences from upstream
  • 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.
  • Roving DOM focus on the options replaces the upstream activeIndex-only highlight (one focus mechanism, PC-12.6).
  • Native `<select>` type-ahead added (printable keys with a 600ms buffer) so a select stays typeable; no attribute or event added.
  • Framer Motion panel, chevron and check springs become the shared spring helper with the pinned springSnappy 380/30/0.5; the AnimatePresence exit is dropped.
  • Form participation added: name, required, reset, default-value and state restoration.
  • Upstream --surface/--border/rose/opacity-30 treatment becomes tokens at full opacity.
  • `error` is presentational (role=alert, aria-invalid, aria-describedby) and does not replace native constraint validation.
  • The row says select, so it is a real button with aria-haspopup=listbox plus role=listbox/option: no menu roles, no text input, no aria-activedescendant.
Previous: Spotlight Search Next: Unfold Panel

Docs

Components

Project