cv-number
Numeric input field with ARIA spinbutton semantics, optional stepper controls, clearable behavior, and prefix/suffix slots.
Headless: createNumber
Usage
Anatomy
<cv-number> (host)
└── <div part="base">
├── <span part="prefix">
│ └── <slot name="prefix">
├── <input part="input" role="spinbutton" inputmode="decimal">
├── <span part="clear-button" role="button"> ← conditional on showClearButton
│ └── <slot name="clear-icon">×</slot>
├── <span part="stepper"> ← conditional on stepper, horizontal controls
│ ├── <button part="increment" type="button">
│ └── <button part="decrement" type="button">
└── <span part="suffix">
└── <slot name="suffix">Attributes
| Attribute | Type | Default | Reflects | Description |
|---|---|---|---|---|
value | Number | 0 | no | Current numeric value |
default-value | Number | min ?? 0 | no | Value to reset to on clear; form reset restores the initial connected value snapshot |
min | Number | — | no | Optional minimum boundary |
max | Number | — | no | Optional maximum boundary |
step | Number | 1 | no | Small increment/decrement step |
large-step | Number | 10 | no | Large increment/decrement step (PageUp/PageDown) |
name | String | "" | no | Form field name for submit serialization |
disabled | Boolean | false | yes | Prevents interaction and dims the component |
read-only | Boolean | false | yes | Keeps focusable/announced but blocks user mutation |
required | Boolean | false | yes | Marks the field as required for form validation |
clearable | Boolean | false | yes | Shows a clear button when the value differs from default |
stepper | Boolean | false | yes | Shows increment/decrement stepper buttons |
placeholder | String | "" | no | Placeholder text displayed when the input is empty |
size | String | "medium" | yes | Component size: small | medium | large |
variant | String | "outlined" | yes | Visual variant: outlined | filled |
aria-label | String | "" | no | Accessible label |
aria-labelledby | String | "" | no | ID reference to visible label |
aria-describedby | String | "" | no | ID reference to description |
Variants
| Variant | Description |
|---|---|
outlined | Default style with visible border and transparent background |
filled | Subtle background fill with no visible border |
Sizes
| Size | --cv-number-height | --cv-number-padding-inline | --cv-number-font-size |
|---|---|---|---|
small | 30px | var(--cv-space-2, 8px) | var(--cv-font-size-sm, 13px) |
medium | 36px | var(--cv-space-3, 12px) | var(--cv-font-size-base, 14px) |
large | 42px | var(--cv-space-4, 16px) | var(--cv-font-size-md, 16px) |
Slots
| Slot | Description |
|---|---|
prefix | Content rendered before the input (e.g., currency symbol icon) |
suffix | Content rendered after the stepper controls (e.g., unit label) |
clear-icon | Custom icon for the clear button (default: ×) |
Note: The native
<input>element is not slottable. There is no default slot.
CSS Parts
| Part | Element | Description |
|---|---|---|
base | <div> | Outermost wrapper element containing input and controls |
input | <input> | The native input element with role="spinbutton" |
prefix | <span> | Wrapper around the prefix slot |
suffix | <span> | Wrapper around the suffix slot |
clear-button | <span> | The clear button wrapper (conditionally visible) |
stepper | <span> | Wrapper around increment/decrement buttons (conditionally visible) |
increment | <button> | Increment stepper button |
decrement | <button> | Decrement stepper button |
CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--cv-number-height | 36px | Component block size |
--cv-number-padding-inline | var(--cv-space-3, 12px) | Horizontal padding inside the input container |
--cv-number-font-size | var(--cv-font-size-base, 14px) | Font size of the input text |
--cv-number-border-radius | var(--cv-radius-sm, 6px) | Border radius of the input container |
--cv-number-border-color | var(--cv-color-border, #2a3245) | Border color in default state |
--cv-number-background | transparent | Background color of the input container |
--cv-number-color | var(--cv-color-text, #e8ecf6) | Text color of the input value |
--cv-number-placeholder-color | var(--cv-color-text-muted, #6b7a99) | Placeholder text color |
--cv-number-focus-ring | 0 0 0 2px var(--cv-color-primary, #65d7ff) | Box-shadow applied on focus |
--cv-number-icon-size | 1em | Size of prefix/suffix/clear icons |
--cv-number-gap | var(--cv-space-2, 8px) | Spacing between inner elements (prefix, input, buttons, suffix) |
--cv-number-transition-duration | var(--cv-duration-fast, 120ms) | Transition duration for state changes |
--cv-number-stepper-width | 28px | Legacy fallback for horizontal stepper button inline size |
--cv-number-stepper-button-inline-size | var(--cv-number-stepper-width, 28px) | Inline size of each horizontal stepper button |
--cv-number-stepper-button-gap | 2px | Gap between horizontal stepper buttons |
Additionally, component styles depend on theme tokens through fallback values:
| Theme Property | Default | Description |
|---|---|---|
--cv-color-border | #2a3245 | Base border color |
--cv-color-surface | #141923 | Surface background color (used by filled variant) |
--cv-color-surface-elevated | #1d2432 | Stepper button background |
--cv-color-text | #e8ecf6 | Default text color |
--cv-color-text-muted | #6b7a99 | Muted text color for placeholder |
--cv-color-primary | #65d7ff | Primary accent color for focus ring |
--cv-duration-fast | 120ms | Transition duration |
--cv-easing-standard | ease | Transition timing function |
--cv-radius-sm | 6px | Base border radius fallback |
--cv-font-size-sm | 13px | Small font size |
--cv-font-size-base | 14px | Base font size |
--cv-font-size-md | 16px | Medium font size |
--cv-space-2 | 8px | Spacing scale: small |
--cv-space-3 | 12px | Spacing scale: medium |
--cv-space-4 | 16px | Spacing scale: large |
Visual States
| Host selector | Description |
|---|---|
:host([disabled]) | Reduced opacity (0.55), cursor: not-allowed, no interaction |
:host([read-only]) | Normal opacity, cursor: default, input text not editable |
:host([required]) | No visual change by default (can be styled via part selectors) |
:host([focused]) | Focus ring applied via --cv-number-focus-ring |
:host([filled]) | Indicates value differs from default (e.g., for floating label transitions) |
:host([clearable]) | Clear button space reserved in layout |
:host([stepper]) | Stepper buttons rendered and visible |
:host([stepper-active="increment"]) | Transient pressed/step feedback for the increment button |
:host([stepper-active="decrement"]) | Transient pressed/step feedback for the decrement button |
:host([size="small"]) | Small size overrides |
:host([size="large"]) | Large size overrides |
:host([variant="outlined"]) | Visible border, transparent background |
:host([variant="filled"]) | Subtle background (--cv-color-surface), no visible border |
Reactive State Mapping
cv-number is a visual adapter over headless createNumber.
UIKit properties to headless actions
| UIKit Property | Direction | Headless Binding |
|---|---|---|
value | attr/prop -> action | actions.setValue(value) |
disabled | attr -> action | actions.setDisabled(value) |
read-only | attr -> action | actions.setReadOnly(value) |
required | attr -> action | actions.setRequired(value) |
placeholder | attr -> action | actions.setPlaceholder(value) |
clearable | attr -> action | actions.setClearable(value) |
stepper | attr -> action | actions.setStepper(value) |
min | attr -> option | passed to createNumber(options) |
max | attr -> option | passed to createNumber(options) |
step | attr -> option | passed to createNumber(options) |
large-step | attr -> option | passed to createNumber(options) |
default-value | attr -> option | passed as defaultValue to createNumber(options) |
aria-label | attr -> option | passed as ariaLabel to createNumber(options) |
aria-labelledby | attr -> option | passed as ariaLabelledBy to createNumber(options) |
aria-describedby | attr -> option | passed as ariaDescribedBy to createNumber(options) |
Headless state to DOM reflection
| Headless State | Direction | DOM Reflection |
|---|---|---|
state.isDisabled() | state -> attr | [disabled] host attribute |
state.isReadOnly() | state -> attr | [read-only] host attribute |
state.required() | state -> attr | [required] host attribute |
state.focused() | state -> attr | [focused] host attribute |
state.filled() | state -> attr | [filled] host attribute |
state.showClearButton() | state -> DOM | shows/hides the clear button element |
state.stepper() | state -> DOM | shows/hides the stepper buttons |
state.draftText() | state -> DOM | when non-null, displayed in the input; when null, displays formatted String(value) |
state.value() | state -> DOM | displayed in the input when draftText is null |
state.placeholder() | state -> DOM | applied as placeholder on the native input |
state.hasMin() / state.hasMax() | state -> DOM | for conditional styling or rendering hints |
Contract props spreading
contracts.getInputProps()is spread onto the[part="input"]native<input>element to applyid,role,tabindex,inputmode,aria-valuenow,aria-valuemin,aria-valuemax,aria-valuetext,aria-disabled,aria-readonly,aria-required,aria-label,aria-labelledby,aria-describedby,placeholder, andautocomplete.contracts.getIncrementButtonProps()is spread onto the[part="increment"]button to applyid,tabindex,aria-label,aria-disabled,hidden,aria-hidden, andonClick.contracts.getDecrementButtonProps()is spread onto the[part="decrement"]button to applyid,tabindex,aria-label,aria-disabled,hidden,aria-hidden, andonClick.contracts.getClearButtonProps()is spread onto the[part="clear-button"]element to applyrole,aria-label,tabindex,hidden,aria-hidden, andonClick.
Event wiring
- Native
<input>inputevent ->actions.handleInput(e.target.value)(updates draft text) - Native
<input>keydownevent ->actions.handleKeyDown(e)(handles ArrowUp/Down, PageUp/Down, Home/End, Enter, Escape) - Native
<input>focusevent ->actions.setFocused(true)-> dispatchescv-focusCustomEvent - Native
<input>blurevent ->actions.setFocused(false)(triggers draft commit) -> dispatchescv-blurCustomEvent; if value changed since focus, dispatchescv-changeCustomEvent - Clear button
click->actions.clear()-> dispatchescv-clearCustomEvent - Increment/decrement button
click-> unified user step path ->actions.increment()/actions.decrement()-> dispatchescv-changeCustomEvent only when the committed value changes - Focused
stepper+ fine-pointer desktop wheel up/down -> normalized threshold accumulator -> unified user step path; modifiers, horizontal-dominant wheel movement, unfocused controls, disabled/read-only controls, and hidden steppers are ignored stepper+ horizontal touch swipe right/left -> thresholded scrub steps through the unified user step path; vertical-dominant touch movement cancels the gesture so page scroll remains available- Stepper button press-and-hold -> delayed repeated steps through the unified user step path, with accelerating repeat interval and click suppression after repeat starts
- Successful touch-origin swipe and long-press steps may call
navigator.vibrate(6)as best-effort feedback when available; vibration is throttled and disabled underprefers-reduced-motion: reduce
Input display logic (UIKit responsibility)
UIKit reads state.draftText() and state.value() to determine what to display in the native <input>:
- When
draftText !== null: displaydraftText(user is actively editing) - When
draftText === null: display formattedString(value)(committed state)
UIKit does not own value management, clamping, snapping, draft commit logic, or ARIA computation; headless state is the source of truth.
Events
| Event | Detail | Description |
|---|---|---|
cv-change | { value: number } | Fires on committed value change from user interaction (stepper click, focused wheel step, touch swipe step, long-press step, keyboard step, draft commit on blur/Enter). Does not fire from programmatic setValue. |
cv-clear | { } | Fires when the value is cleared via the clear button or Escape key |
cv-focus | { } | Fires when the input receives focus |
cv-blur | { } | Fires when the input loses focus |
Imperative API
| Method / Property | Description |
|---|---|
stepUp(times = 1) | Increments by step times times |
stepDown(times = 1) | Decrements by step times times |
pageUp(times = 1) | Increments by largeStep times times |
pageDown(times = 1) | Decrements by largeStep times times |
setValue(value) | Sets numeric value through headless normalization |
getValue() | Returns current committed numeric value |
setRange(min, max) | Updates range boundaries; null/undefined removes a bound |
focus(options?) | Focuses inner input control |
select() | Selects text in inner input control |
checkValidity() | Runs current validation checks |
reportValidity() | Reports validation state to UA when supported |
setCustomValidity(message) | Sets/clears custom validity message |
form | Form owner when form-associated internals are supported |
validity | Current validity state when supported |
validationMessage | Current validation message |
willValidate | Whether control participates in validation |
Form Association
- Component is form-associated via
ElementInternalswhen available. - Submit value is serialized as the committed numeric value string.
disabledstate removes form value from submission.- Reset restores the initial
valuesnapshot captured on first connection. - Clear button and
Escapereset todefault-value(min ?? 0when omitted), which is separate from form reset. - Browser form-state restoration accepts numeric string state and ignores non-numeric state.