A number input with buttons to step the value down and up.
import { NumberField } from '@oztix/roadie-components/number-field'
With no children, NumberField renders the whole stepper: a decrease button, the input and an increase button. Set min and max and each button turns off at its end of the range. It's only as wide as its widest value, worked out from min, max and format (3 characters when there's no max), so the buttons don't move as the number grows. A value people can type into is never narrower than 2.75rem, or 3.5rem on touch screens, so it's easy to tap. Pass className='w-full' to stretch it.
<NumberField aria-label='Tickets' defaultValue={1} min={0} max={10} />
normal is the sunken field surface that Input uses. subtle swaps the border for a tinted fill. subtler drops the field box: two round buttons, sized like IconButton, sit either side of the value. The value keeps a small tinted chip that behaves like a subtle Input, so people can see they can tap it to type.
<div className='grid max-w-40 gap-4'> <NumberField aria-label='Normal' defaultValue={1} min={0} /> <NumberField aria-label='Subtle' defaultValue={1} min={0} emphasis='subtle' /> <NumberField aria-label='Subtler' defaultValue={1} min={0} emphasis='subtler' /> </div>
Each button is as tall as the field, and that whole square is the tap area. On touch checkouts, use lg for a 48px target.
<div className='grid max-w-40 gap-4'> <NumberField aria-label='Small' size='sm' defaultValue={1} min={0} /> <NumberField aria-label='Medium' size='md' defaultValue={1} min={0} /> <NumberField aria-label='Large' size='lg' defaultValue={1} min={0} /> </div>
The group uses is-interactive-field-group: neutral at rest, accent on focus, danger when invalid. A button at min or max dims and stops responding.
Three ways to limit a field. editable={false} stops typing but the buttons and arrow keys still step. readOnly stops typing and stepping, and the value still submits with the form. disabled turns the whole field off and leaves it out of the form.
Wrap it in Field for the label, helper text and spacing. Field.Label points at the input, and the helper text becomes its description.
<Field required className='max-w-48'> <Field.Label showIndicator>Parking passes</Field.Label> <NumberField min={0} max={4} defaultValue={1} /> <Field.HelperText>Up to 4 per order</Field.HelperText> </Field>
Set invalid on the Field. The group turns danger and the input points at the error text.
<Field invalid required className='max-w-48'> <Field.Label showIndicator>Tickets</Field.Label> <NumberField min={0} max={10} defaultValue={0} /> <Field.ErrorText>Choose at least 1 ticket.</Field.ErrorText> </Field>
Pass children to lay the parts out yourself. Keep NumberField.Decrement, NumberField.Input and NumberField.Increment inside NumberField.Group, which draws the field or, at subtler, spaces the buttons. Here the buttons sit after the input, and NumberField.ScrubArea lets people drag the label left or right to change the value. Give the root an id so the label can point at the input.
The checkout pattern. Each ticket type gets a stepper from 0 to its limit, and the order total follows the values. Label each stepper with its ticket name.
For carts and add-on lists, compose the subtler look. Decrement and Increment take emphasis and intent like IconButton, so the increase button can carry the accent. With removable, the decrease button turns into a Remove button with a trash icon when one more step would reach min. It still steps down.
Set editable={false} when the value should only change with the buttons, such as an add-on limited to a few. The chip goes and the value hugs the number. The arrow keys, Home and End still step, unlike readOnly, which stops every change.
The digits roll to each new value with NumberFlow, in every emphasis and with the same format and locale as the input. The animated number is a visual copy hidden from screen readers. Tap into the input or start typing and its own text fades in for editing. The next button press, arrow key or scroll fades back to the animation. People who set their system to reduce motion see the value change without the roll. Scientific and engineering notation don't animate.
format takes Intl.NumberFormat options. The input shows the formatted value and parses what people type back to a number, so onValueChange gets a plain number, or null when people clear the field. Set locale so the server and browser format the same way.
<NumberField min={0} max={10} />
Do
Use a stepper for quantities people change by one or two, like tickets and add-ons. Always set min and max so the buttons show the limits.
<NumberField aria-label='Postcode' />
Don’t
Don't use it for numbers that aren't amounts, such as postcodes, phone numbers or card numbers. Use Input with inputMode='numeric'.
<NumberField format={{ style: 'currency', currency: 'AUD' }} />
Do
Pass Intl options so the value reads as money or a percentage and still comes back as a number.
Don’t
Don't put a dollar sign or unit in the label or beside the input by hand. It won't match the locale and people may type it twice.
aria-roledescription='Number field'. Name it with Field.Label, a <label>, aria-label or aria-labelledby. Without children, aria-label and aria-labelledby on the root go to the input.step. Shift steps by largeStep (10), Alt by smallStep (0.1). Home and End jump to min and max.subtler with editable={false}, or on the field box. A mouse press on a button moves focus to the input so the arrow keys keep working, but shows no ring until the person types or uses the keyboard.removable, the decrease button is named Remove when the next step reaches min, so screen readers announce what it does.aria-hidden, and the animation respects prefers-reduced-motion.editable={false} makes the input read only for typing, so screen readers announce it as read only, but the buttons and arrow keys still step. Use readOnly when the value must not change at all.subtler the value chip turns danger when invalid. With editable={false} there's no chip, so show Field.ErrorText as well.The id of the input element.
The minimum value of the input element.
The maximum value of the input element.
When true, direct text entry may be outside the `min`/`max` range without clamping, so native range underflow/overflow validation can occur. Step-based interactions (keyboard arrows, buttons, wheel, scrub) still clamp.
Defaults to false.
The small step value of the input element when incrementing while the alt key is held. Snaps to multiples of this value when `snapOnStep` is enabled.
Defaults to 0.1.
Amount to increment and decrement with the buttons and arrow keys, or to scrub with pointer movement in the scrub area. To always enable step validation on form submission, specify the `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`.
Defaults to 1.
The large step value of the input element when incrementing while the shift key is held. Snaps to multiples of this value when `snapOnStep` is enabled.
Defaults to 10.
Whether the user must enter a value before submitting a form.
Defaults to false.
Whether the component should ignore user interaction.
Defaults to false.
Whether the user should be unable to change the field value.
Defaults to false.
Identifies the field when a form is submitted.
Identifies the form that owns the hidden input. Useful when the number field is rendered outside the form.
The raw numeric value of the field.
The uncontrolled value of the field when it's initially rendered. To render a controlled number field, use the `value` prop instead.
Whether to allow the user to scrub the input value with the mouse wheel while focused and hovering over the input.
Defaults to false.
Whether the value should snap to the nearest step when incrementing or decrementing.
Defaults to false.
Options to format the input value.
Callback fired when the number value changes. The `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
Callback function that is fired when the value is committed. It runs later than `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.
The locale of the input element. Defaults to the user's runtime locale.
A ref to access the hidden input element.
CSS class applied to the element, or a function that returns a class based on the component's state.
Height of the field and its stepper buttons.
Field surface. `subtle` swaps the border for a tinted fill. `subtler` drops the field box for round standalone buttons either side of a bare number.
Marks the value as invalid. Inherits from `Field` when omitted.
One step above `min`, the decrement button shows a trash icon and is named Remove. It still steps down to `min`.
Whether people can type a value. With `false` the buttons, arrow keys, Home and End still change it.
Defaults to true.
CSS class applied to the element, or a function that returns a class based on the component's state.
Button surface. Defaults to `subtler` in the field, `normal` standalone.
Colour palette for the button. Inherits from context when omitted.
Inherited from NativeButtonProps
Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).
Defaults to true.
CSS class applied to the element, or a function that returns a class based on the component's state.
Overrides the size set on the root.
Overrides the emphasis set on the root.
CSS class applied to the element, or a function that returns a class based on the component's state.
Button surface. Defaults to `subtler` in the field, `normal` standalone.
Colour palette for the button. Inherits from context when omitted.
Inherited from NativeButtonProps
Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).
Defaults to true.
The input and an animated copy of its value share one grid cell. The animated number is the visible layer; the input's text only shows once someone taps into it or its text changes, and any step after that hands the display back to the animation. Focus and key presses alone don't count: Base UI focuses the input whenever a mouse presses a stepper button, and it drops keys that aren't part of a number.
CSS class applied to the element, or a function that returns a class based on the component's state.
Inherited from NumberFieldInputProps
A user-friendly description of the input's role for assistive tech. This is a role description, not an accessible name — use `Field.Label` or `aria-label` to name the control.
Defaults to 'Number field'.
CSS class applied to the element, or a function that returns a class based on the component's state.
Inherited from NumberFieldScrubAreaProps
Cursor movement direction in the scrub area.
Defaults to 'horizontal'.
Determines how many pixels the cursor must move before the value changes. A higher value will make scrubbing less sensitive.
Defaults to 2.
If specified, determines the distance that the cursor may move from the center of the scrub area before it will loop back around.
No additional props. It forwards all standard HTML attributes to the underlying element.