Number Field
A numeric input element with increment and decrement buttons, and a scrub area.
import { createUniqueId } from 'solid-js';
import type { JSX } from '@solidjs/web';
import { NumberField } from 'base-ui-solid/number-field';
import styles from './index.module.css';
export default function ExampleNumberField() {
const id = createUniqueId();
return (
<NumberField.Root id={id} defaultValue={100} class={styles.Field}>
<NumberField.ScrubArea class={styles.ScrubArea}>
<label for={id} class={styles.Label}>
Amount
</label>
<NumberField.ScrubAreaCursor class={styles.ScrubAreaCursor}>
<CursorGrowIcon />
</NumberField.ScrubAreaCursor>
</NumberField.ScrubArea>
<NumberField.Group class={styles.Group}>
<NumberField.Decrement class={styles.Decrement}>
<MinusIcon />
</NumberField.Decrement>
<NumberField.Input class={styles.Input} />
<NumberField.Increment class={styles.Increment}>
<PlusIcon />
</NumberField.Increment>
</NumberField.Group>
</NumberField.Root>
);
}
function CursorGrowIcon(
props: Omit<JSX.IntrinsicElements['svg'], 'style'> & { style?: JSX.CSSProperties },
) {
return (
<svg
width="26"
height="14"
viewBox="0 0 24 14"
fill="black"
stroke="white"
{...props}
style={{ display: 'block', ...props.style }}
>
<path d="M19.5 5.5L6.49737 5.51844V2L1 6.9999L6.5 12L6.49737 8.5L19.5 8.5V12L25 6.9999L19.5 2V5.5Z" />
</svg>
);
}
function PlusIcon(
props: Omit<JSX.IntrinsicElements['svg'], 'style'> & { style?: JSX.CSSProperties },
) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-linecap="square"
stroke-linejoin="round"
{...props}
style={{ display: 'block', ...props.style }}
>
<path d="M1.5 8h13M8 14.5v-13" />
</svg>
);
}
function MinusIcon(
props: Omit<JSX.IntrinsicElements['svg'], 'style'> & { style?: JSX.CSSProperties },
) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-linecap="square"
stroke-linejoin="round"
{...props}
style={{ display: 'block', ...props.style }}
>
<path d="M1.5 8h13" />
</svg>
);
}
Usage guidelines
- Form controls must have an accessible name: It can be created using a
<label>element or theFieldcomponent. See the forms guide.
Anatomy
Import the component and assemble its parts:
import { NumberField } from 'base-ui-solid/number-field';
<NumberField.Root>
<NumberField.ScrubArea>
<NumberField.ScrubAreaCursor />
</NumberField.ScrubArea>
<NumberField.Group>
<NumberField.Decrement />
<NumberField.Input />
<NumberField.Increment />
</NumberField.Group>
</NumberField.Root>;
API reference
Root
Groups all parts of the number field and manages its state.
Renders a <div> element.
namestring—
stringdefaultValuenumber—
value prop instead.numbervalueUnion—
number | nullonValueChangefunction—
eventDetails.reason indicates what triggered the change: 'input-change' for parseable typing or programmatic text updates'input-clear' when the field becomes empty'input-blur' when formatting (and clamping, if enabled) occurs on blur'input-paste' for paste interactions'keyboard' for arrow-key/Home/End stepping (typing digits uses 'input-change'/'input-clear')'increment-press' / 'decrement-press' for button presses on the increment and decrement controls'wheel' for wheel-based scrubbing'scrub' for scrub area drags((value: number | null, eventDetails: NumberField.Root.ChangeEventDetails) => void)onValueCommittedfunction—
onValueChange, when: The input is blurred after typing a value.The pointer is released after scrubbing or pressing the increment/decrement buttons. It runs simultaneously with onValueChange when interacting with the keyboard or the
mouse wheel. **Warning**: This is a generic event not a change event.((value: number | null, eventDetails: NumberField.Root.CommitEventDetails) => void)allowOutOfRangebooleanfalse
min/max range without clamping,
so native range underflow/overflow validation can occur.
Step-based interactions (keyboard arrows, buttons, wheel, scrub) still clamp.booleanformstring—
stringlocaleIntl.LocalesArgument—
Intl.LocalesArgumentsnapOnStepbooleanfalse
booleanstepUnion1
min prop explicitly in conjunction with this prop.
Specify step="any" to always disable step validation; interactive stepping then uses a base amount of 1, while the alt and shift keys still step by smallStep and largeStep.number | 'any'smallStepnumber0.1
snapOnStep is enabled.numberlargeStepnumber10
snapOnStep is enabled.numberminnumber—
numbermaxnumber—
numberallowWheelScrubbooleanfalse
booleanformatIntl.NumberFormatOptions—
Intl.NumberFormatOptionsdisabledbooleanfalse
booleanreadOnlybooleanfalse
booleanrequiredbooleanfalse
booleaninputRefRef<HTMLInputElement>—
Ref<HTMLInputElement>idstring—
stringclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-disabled-—
-data-readonly-—
-data-required-—
-data-valid-—
-data-invalid-—
-data-dirty-—
-data-touched-—
-data-filled-—
-data-focused-—
-data-scrubbing-—
-Attribute | Description | |
|---|---|---|
data-disabled | Present when the number field is disabled. | |
data-readonly | Present when the number field is readonly. | |
data-required | Present when the number field is required. | |
data-valid | Present when the number field is in a valid state (when wrapped in Field.Root). | |
data-invalid | Present when the number field is in an invalid state (when wrapped in Field.Root). | |
data-dirty | Present when the number field’s value has changed (when wrapped in Field.Root). | |
data-touched | Present when the number field has been touched (when wrapped in Field.Root). | |
data-filled | Present when the number field is filled (when wrapped in Field.Root). | |
data-focused | Present when the number field is focused (when wrapped in Field.Root). | |
data-scrubbing | Present while scrubbing. | |
Root.State
type NumberFieldRootState = {
/** The raw numeric value of the field. */
value: number | null;
/** The formatted string value presented in the input element. */
inputValue: string;
/** Whether the user must enter a value before submitting a form. */
required: boolean;
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the user should be unable to change the field value. */
readOnly: boolean;
/** Whether the user is currently scrubbing the field. */
scrubbing: boolean;
/** Whether the field has been touched. */
touched: boolean;
/** Whether the field value has changed from its initial value. */
dirty: boolean;
/** Whether the field is valid. */
valid: boolean | null;
/** Whether the field has a value. */
filled: boolean;
/** Whether the field is focused. */
focused: boolean;
};Root.ChangeEventReason
type NumberFieldRootChangeEventReason =
| 'input-change'
| 'input-clear'
| 'input-blur'
| 'input-paste'
| 'keyboard'
| 'increment-press'
| 'decrement-press'
| 'wheel'
| 'scrub'
| 'none';Root.ChangeEventDetails
type NumberFieldRootChangeEventDetails = (
| { reason: 'input-change'; event: InputEvent | Event }
| { reason: 'input-clear'; event: InputEvent | Event | FocusEvent }
| { reason: 'input-blur'; event: FocusEvent }
| { reason: 'input-paste'; event: ClipboardEvent }
| { reason: 'keyboard'; event: KeyboardEvent }
| { reason: 'increment-press'; event: PointerEvent | MouseEvent | TouchEvent }
| { reason: 'decrement-press'; event: PointerEvent | MouseEvent | TouchEvent }
| { reason: 'wheel'; event: WheelEvent }
| { reason: 'scrub'; event: PointerEvent }
| { reason: 'none'; event: Event }
) & {
/** Cancels Base UI from handling the event. */
cancel: () => void;
/** Allows the event to propagate in cases where Base UI will stop the propagation. */
allowPropagation: () => void;
/** Indicates whether the event has been canceled. */
isCanceled: boolean;
/** Indicates whether the event is allowed to propagate. */
isPropagationAllowed: boolean;
/** The element that triggered the event, if applicable. */
trigger: Element | undefined;
direction?: Direction;
};Root.CommitEventReason
type NumberFieldRootCommitEventReason =
| 'input-blur'
| 'input-clear'
| 'keyboard'
| 'increment-press'
| 'decrement-press'
| 'wheel'
| 'scrub'
| 'none';Root.CommitEventDetails
type NumberFieldRootCommitEventDetails =
| { reason: 'input-clear'; event: InputEvent | Event | FocusEvent }
| { reason: 'input-blur'; event: FocusEvent }
| { reason: 'keyboard'; event: KeyboardEvent }
| { reason: 'increment-press'; event: PointerEvent | MouseEvent | TouchEvent }
| { reason: 'decrement-press'; event: PointerEvent | MouseEvent | TouchEvent }
| { reason: 'wheel'; event: WheelEvent }
| { reason: 'scrub'; event: PointerEvent }
| { reason: 'none'; event: Event };ScrubArea
An interactive area where the user can click and drag to change the field value.
Renders a <span> element.
directionUnion'horizontal'
'horizontal' | 'vertical'pixelSensitivitynumber2
numberteleportDistancenumber—
numberclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-disabled-—
-data-readonly-—
-data-required-—
-data-valid-—
-data-invalid-—
-data-dirty-—
-data-touched-—
-data-filled-—
-data-focused-—
-data-scrubbing-—
-Attribute | Description | |
|---|---|---|
data-disabled | Present when the number field is disabled. | |
data-readonly | Present when the number field is readonly. | |
data-required | Present when the number field is required. | |
data-valid | Present when the number field is in a valid state (when wrapped in Field.Root). | |
data-invalid | Present when the number field is in an invalid state (when wrapped in Field.Root). | |
data-dirty | Present when the number field’s value has changed (when wrapped in Field.Root). | |
data-touched | Present when the number field has been touched (when wrapped in Field.Root). | |
data-filled | Present when the number field is filled (when wrapped in Field.Root). | |
data-focused | Present when the number field is focused (when wrapped in Field.Root). | |
data-scrubbing | Present while scrubbing. | |
ScrubArea.State
type NumberFieldScrubAreaState = {
/** The raw numeric value of the field. */
value: number | null;
/** The formatted string value presented in the input element. */
inputValue: string;
/** Whether the user must enter a value before submitting a form. */
required: boolean;
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the user should be unable to change the field value. */
readOnly: boolean;
/** Whether the user is currently scrubbing the field. */
scrubbing: boolean;
/** Whether the field has been touched. */
touched: boolean;
/** Whether the field value has changed from its initial value. */
dirty: boolean;
/** Whether the field is valid. */
valid: boolean | null;
/** Whether the field has a value. */
filled: boolean;
/** Whether the field is focused. */
focused: boolean;
};ScrubAreaCursor
A custom element to display instead of the native cursor while using the scrub area.
Renders a <span> element.
This component uses the [Pointer Lock API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API), which may prompt the browser to display a related notification. It is disabled
in Safari to avoid a layout shift that this notification causes there.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-disabled-—
-data-readonly-—
-data-required-—
-data-valid-—
-data-invalid-—
-data-dirty-—
-data-touched-—
-data-filled-—
-data-focused-—
-data-scrubbing-—
-Attribute | Description | |
|---|---|---|
data-disabled | Present when the number field is disabled. | |
data-readonly | Present when the number field is readonly. | |
data-required | Present when the number field is required. | |
data-valid | Present when the number field is in a valid state (when wrapped in Field.Root). | |
data-invalid | Present when the number field is in an invalid state (when wrapped in Field.Root). | |
data-dirty | Present when the number field’s value has changed (when wrapped in Field.Root). | |
data-touched | Present when the number field has been touched (when wrapped in Field.Root). | |
data-filled | Present when the number field is filled (when wrapped in Field.Root). | |
data-focused | Present when the number field is focused (when wrapped in Field.Root). | |
data-scrubbing | Present while scrubbing. | |
ScrubAreaCursor.State
type NumberFieldScrubAreaCursorState = {
/** The raw numeric value of the field. */
value: number | null;
/** The formatted string value presented in the input element. */
inputValue: string;
/** Whether the user must enter a value before submitting a form. */
required: boolean;
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the user should be unable to change the field value. */
readOnly: boolean;
/** Whether the user is currently scrubbing the field. */
scrubbing: boolean;
/** Whether the field has been touched. */
touched: boolean;
/** Whether the field value has changed from its initial value. */
dirty: boolean;
/** Whether the field is valid. */
valid: boolean | null;
/** Whether the field has a value. */
filled: boolean;
/** Whether the field is focused. */
focused: boolean;
};Group
Groups the input with the increment and decrement buttons.
Renders a <div> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-disabled-—
-data-readonly-—
-data-required-—
-data-valid-—
-data-invalid-—
-data-dirty-—
-data-touched-—
-data-filled-—
-data-focused-—
-data-scrubbing-—
-Attribute | Description | |
|---|---|---|
data-disabled | Present when the number field is disabled. | |
data-readonly | Present when the number field is readonly. | |
data-required | Present when the number field is required. | |
data-valid | Present when the number field is in a valid state (when wrapped in Field.Root). | |
data-invalid | Present when the number field is in an invalid state (when wrapped in Field.Root). | |
data-dirty | Present when the number field’s value has changed (when wrapped in Field.Root). | |
data-touched | Present when the number field has been touched (when wrapped in Field.Root). | |
data-filled | Present when the number field is filled (when wrapped in Field.Root). | |
data-focused | Present when the number field is focused (when wrapped in Field.Root). | |
data-scrubbing | Present while scrubbing. | |
Group.State
type NumberFieldGroupState = {
/** The raw numeric value of the field. */
value: number | null;
/** The formatted string value presented in the input element. */
inputValue: string;
/** Whether the user must enter a value before submitting a form. */
required: boolean;
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the user should be unable to change the field value. */
readOnly: boolean;
/** Whether the user is currently scrubbing the field. */
scrubbing: boolean;
/** Whether the field has been touched. */
touched: boolean;
/** Whether the field value has changed from its initial value. */
dirty: boolean;
/** Whether the field is valid. */
valid: boolean | null;
/** Whether the field has a value. */
filled: boolean;
/** Whether the field is focused. */
focused: boolean;
};Decrement
A stepper button that decreases the field value when clicked.
Renders a <button> element.
nativeButtonbooleantrue
<button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (for example, <div>).booleanclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-disabled-—
-data-readonly-—
-data-required-—
-data-valid-—
-data-invalid-—
-data-dirty-—
-data-touched-—
-data-filled-—
-data-focused-—
-data-scrubbing-—
-Attribute | Description | |
|---|---|---|
data-disabled | Present when the number field is disabled. | |
data-readonly | Present when the number field is readonly. | |
data-required | Present when the number field is required. | |
data-valid | Present when the number field is in a valid state (when wrapped in Field.Root). | |
data-invalid | Present when the number field is in an invalid state (when wrapped in Field.Root). | |
data-dirty | Present when the number field’s value has changed (when wrapped in Field.Root). | |
data-touched | Present when the number field has been touched (when wrapped in Field.Root). | |
data-filled | Present when the number field is filled (when wrapped in Field.Root). | |
data-focused | Present when the number field is focused (when wrapped in Field.Root). | |
data-scrubbing | Present while scrubbing. | |
Decrement.State
type NumberFieldDecrementState = {
/** The raw numeric value of the field. */
value: number | null;
/** The formatted string value presented in the input element. */
inputValue: string;
/** Whether the user must enter a value before submitting a form. */
required: boolean;
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the user should be unable to change the field value. */
readOnly: boolean;
/** Whether the user is currently scrubbing the field. */
scrubbing: boolean;
/** Whether the field has been touched. */
touched: boolean;
/** Whether the field value has changed from its initial value. */
dirty: boolean;
/** Whether the field is valid. */
valid: boolean | null;
/** Whether the field has a value. */
filled: boolean;
/** Whether the field is focused. */
focused: boolean;
};Input
The native input control in the number field.
Renders an <input> element.
aria-roledescriptionstring'Number field'
Field.Label or aria-label to name the control.stringclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-disabled-—
-data-readonly-—
-data-required-—
-data-valid-—
-data-invalid-—
-data-dirty-—
-data-touched-—
-data-filled-—
-data-focused-—
-data-scrubbing-—
-Attribute | Description | |
|---|---|---|
data-disabled | Present when the number field is disabled. | |
data-readonly | Present when the number field is readonly. | |
data-required | Present when the number field is required. | |
data-valid | Present when the number field is in a valid state (when wrapped in Field.Root). | |
data-invalid | Present when the number field is in an invalid state (when wrapped in Field.Root). | |
data-dirty | Present when the number field’s value has changed (when wrapped in Field.Root). | |
data-touched | Present when the number field has been touched (when wrapped in Field.Root). | |
data-filled | Present when the number field is filled (when wrapped in Field.Root). | |
data-focused | Present when the number field is focused (when wrapped in Field.Root). | |
data-scrubbing | Present while scrubbing. | |
Input.State
type NumberFieldInputState = {
/** The raw numeric value of the field. */
value: number | null;
/** The formatted string value presented in the input element. */
inputValue: string;
/** Whether the user must enter a value before submitting a form. */
required: boolean;
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the user should be unable to change the field value. */
readOnly: boolean;
/** Whether the user is currently scrubbing the field. */
scrubbing: boolean;
/** Whether the field has been touched. */
touched: boolean;
/** Whether the field value has changed from its initial value. */
dirty: boolean;
/** Whether the field is valid. */
valid: boolean | null;
/** Whether the field has a value. */
filled: boolean;
/** Whether the field is focused. */
focused: boolean;
};Increment
A stepper button that increases the field value when clicked.
Renders a <button> element.
nativeButtonbooleantrue
<button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (for example, <div>).booleanclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-disabled-—
-data-readonly-—
-data-required-—
-data-valid-—
-data-invalid-—
-data-dirty-—
-data-touched-—
-data-filled-—
-data-focused-—
-data-scrubbing-—
-Attribute | Description | |
|---|---|---|
data-disabled | Present when the number field is disabled. | |
data-readonly | Present when the number field is readonly. | |
data-required | Present when the number field is required. | |
data-valid | Present when the number field is in a valid state (when wrapped in Field.Root). | |
data-invalid | Present when the number field is in an invalid state (when wrapped in Field.Root). | |
data-dirty | Present when the number field’s value has changed (when wrapped in Field.Root). | |
data-touched | Present when the number field has been touched (when wrapped in Field.Root). | |
data-filled | Present when the number field is filled (when wrapped in Field.Root). | |
data-focused | Present when the number field is focused (when wrapped in Field.Root). | |
data-scrubbing | Present while scrubbing. | |
Increment.State
type NumberFieldIncrementState = {
/** The raw numeric value of the field. */
value: number | null;
/** The formatted string value presented in the input element. */
inputValue: string;
/** Whether the user must enter a value before submitting a form. */
required: boolean;
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the user should be unable to change the field value. */
readOnly: boolean;
/** Whether the user is currently scrubbing the field. */
scrubbing: boolean;
/** Whether the field has been touched. */
touched: boolean;
/** Whether the field value has changed from its initial value. */
dirty: boolean;
/** Whether the field is valid. */
valid: boolean | null;
/** Whether the field has a value. */
filled: boolean;
/** Whether the field is focused. */
focused: boolean;
};