Forms & inputs
SpringSelect
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
hoverrecorded state; no setterdisabledrecorded state; no setterclosedrecorded state; no setterexpandedrecorded state; no setterselectedrecorded state; no setterdark themerecorded state; no setteractiverecorded 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-valuedisablederrorlabelnameplaceholderrequiredvalue
Properties
optionsvaluedefaultValueplaceholderlabelerrordisabledrequirednameopenonChange
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.tsxsha256 9cf1a4d482c2…
Native files
The native module and its artifacts:
src/components/spring-select/spring-select.tssrc/components/spring-select/styles.csssrc/components/spring-select/define.tsdocs/components/spring-select.mdsite/examples/spring-select.tstests/browser/spring-select.spec.tsevidence/components/spring-select.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 |
|---|---|---|---|---|
default-value | defaultValue | string | undefined | Kebab-case attribute; camelCase property. |
disabled | disabled | boolean | false | Presence semantics: present means true, absent means false. |
error | error | string | undefined | Kebab-case attribute; camelCase property. |
label | label | string | undefined | 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. |
value | value | string | undefined | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
options | SpringSelectOption[] | [] | Property-only: assign from script; not accepted as an attribute. |
value | string | undefined | Property-only: assign from script; not accepted as an attribute. |
defaultValue | string | undefined | Property-only: assign from script; not accepted as an attribute. |
placeholder | string | 'Select…' | Property-only: assign from script; not accepted as an attribute. |
label | string | undefined | Property-only: assign from script; not accepted as an attribute. |
error | string | undefined | 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. |
onChange | (value: string) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-change | onChange | (value: string) => void | Composed, 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.