Numeric input with attached increment and decrement buttons for tight quantity controls.
Description
InputStepper is a number-only input flanked by - and + buttons. It renders a focusable <input type="number"> at its center and shares the input shell context with its sub-parts, so size, invalid, and disabled cascade from a parent Field without manual prop drilling.
Reach for it when the value is a bounded count: cart line items, seat selectors, batch sizes, font weights, page numbers. Use variant="connected" for compact form rows where the buttons should read as part of the input shell; use variant="separated" when the buttons need more tap-area on touch surfaces or should visually detach from the cell.
Don't use it for free-form numeric entry without bounds; drop down to Input with type="number". For wide ranges chosen by feel rather than precise value, use a slider. For binary on/off, use Switch or Checkbox. If the value is picked from a fixed list, use Select.
Installation
Usage
Examples
Variants
variant="connected" (default) shares one shell with bordered buttons that read as part of the input cell. variant="separated" renders three pieces with a gap, which lifts the buttons into their own tap targets, useful on touch surfaces.
Sizes
Three sizes (xs, sm, md) control the cell height, padding, and type scale, plus the button size that pairs with it. sm is the default.
States
invalid and disabled flow through the input context to the cell, the input, and both buttons. The default row is empty so the placeholder shows through in placeholder color.
Bounded range
Set min, max, and step to clamp the value and tick by a custom interval. The decrement button auto-disables when the current value is <= min and the increment button when it's >= max.
Composition with Field
Wrap the stepper in Field to add a label and helper text. size, invalid, and disabled on Field cascade to the stepper through the shared input context, so the inner control never has to repeat those props.
Controlled
Drive the value from state by combining value and onValueChange. The callback fires with the clamped result of every commit, whether the source is typed entry, a +/- click, or a native arrow-key step.
Accessibility
Focus stays on the input cell: the - / + buttons are tabIndex={-1} and call preventDefault on mousedown so a click fires without stealing focus. Arrow-key stepping is owned by the native <input type="number">.
ARIA notes:
- Decrement and increment buttons set
aria-label; override the defaults with thedecrementLabelandincrementLabelprops. - The separated variant wraps the trio in a
role="group"element so assistive tech announces it as a single control. - The input forwards
aria-invalidwheninvalidistruedirectly or inherited from a parentField. - Buttons keep focus on the input via
onMouseDown={preventDefault}. This is intentional, not a bug: the focus ring stays on the cell while clicks still fire. - For label association, pass an
idand useFieldLabel htmlFor(see theFieldcomposition example above).
Styling
Tailwind override: pass className to constrain or restyle the root via cn():
Data slots and attributes: the component sets these for CSS targeting:
data-slot="input-stepper"on the root (InputShellforconnected,divforseparated).data-variant="connected" | "separated"on the root.data-slot="input-stepper-decrement"anddata-slot="input-stepper-increment"on the buttons.data-slot="input-stepper-cell"on the center cell.data-slot="input-stepper-control"on the inner<input>.data-slot="input-stepper-cell-shell"on the wrapper shell (separated variant only).- Inherited from
Fieldvia the input shell context:data-size,data-invalid,data-disabled,data-loading.
Target a sub-part in CSS:
Related Components
- Input: free-form text or numeric entry without bounds or stepper buttons.
- Field: wraps the stepper to add label, description, and error footer; cascades
size,invalid, anddisabledautomatically. - InputGroup: a separate composite for building custom adornment clusters (icons, addons, buttons) around
Input.
API Reference
InputStepper
Numeric stepper input. Extends React.ComponentProps<"input"> minus size, value, defaultValue, and onChange (managed internally), so standard input attributes like id, name, placeholder, required, and aria-* flow through to the inner <input>.