Number Input
Primitive

Number Input

Headless numeric text input with spinbutton semantics.

Live behavior

Number Input in motion

Interactive
Atom behavior · App-owned appearance
Preparing behavior…

Interact with the specimen and inspect the behavior Atom contributes.

waiting for input

When to Use

Use NumberInput when a value is a number and people should be able to type it or step it up and down with the keyboard, such as quantity or percentage. Use Input when the text only looks numeric, such as a postal code, account number, or phone number, because those values should not be incremented or clamped.

Features

The behavior Atom owns before your product adds appearance.

  • Renders an editable text input with role="spinbutton".
  • Can be controlled or uncontrolled.
  • Supports min, max, step, largeStep, and precision formatting.
  • Supports keyboard stepping with arrows, Page Up/Down, Home, and End.
  • Supports custom parser and formatter functions.
  • Renders a hidden input for native form submission when named.
  • Provides Input, Increment, and Decrement parts for compound control layouts.

Import

tsx
import { NumberInput } from "@flowstack-ui/atom";

Anatomy

tsx
<NumberInput.Root>
  <NumberInput.Decrement />
  <NumberInput.Input />
  <NumberInput.Increment />
</NumberInput.Root>

API Reference

Root

Renders the root container, inner spinbutton input, and optional hidden form input.

PropTypeDefault
valuenumber | null-
defaultValuenumber-
onValueChange(value: number | null) => void-
minnumber-
maxnumber-
stepnumber1
largeStepnumberstep * 10
precisionnumberInferred from step
clampOnBlurbooleantrue
formatter(value: string) => string-
parser(displayValue: string) => string-
disabledbooleanField state or false
readOnlybooleanField state or false
requiredbooleanField state or false
invalidbooleanField state or false
validationBehavior"inline" | "native"Field/Form value or "native"
placeholderstring-
namestring-
formstring-
idstring-
aria-labelstringField label relationship
aria-valuetextstring | (value: number) => string-
aria-describedbystringField messages
classNamestring-
inputClassNamestring-
childrenReactNode | (state: NumberInputRenderState) => ReactNode-
ARIA attributeValues
aria-labelNative value when provided
aria-valuenowCurrent numeric value when not empty
aria-valueminValue from min
aria-valuemaxValue from max
aria-valuetextNative string or callback result
aria-describedbyNative value or inherited Field messages
aria-invalidPresent when invalid
aria-readonlyPresent when read only
aria-requiredPresent when required
Data attributeValues
[data-slot]"number-input"
[data-disabled]Present when disabled
[data-readonly]Present when read-only
[data-invalid]Present when invalid

When children is a function, it receives numericValue, displayValue, isAtMin, isAtMax, disabled, readOnly, handleStep, and inputRef. This preserves the legacy render-callback path for custom controls.

When Root has no children or uses the legacy render callback, it renders its Input automatically. Static children opt into compound anatomy and should include exactly one Input.

Input

Renders the editable spinbutton. Native input props, render, asChild, and a native input ref are supported. Root owns value, limits, form state, and the generated Field relationships.

Increment and Decrement

Render native buttons that call Root's step behavior, preserve input focus on pointer activation, reference the Input with aria-controls, and expose aria-disabled plus [data-boundary] at a known limit. They default to tabIndex={-1} while remaining available to pointer, touch, and voice access. Provide localized action labels when the English Increment and Decrement defaults are not appropriate.

Examples

Basic Range

tsx
import { NumberInput } from "@flowstack-ui/atom";

export function QuantityInput() {
  return <NumberInput.Root aria-label="Quantity" min={0} max={10} step={1} />;
}

Currency Formatting

tsx
import { NumberInput } from "@flowstack-ui/atom";

export function CurrencyInput() {
  return (
    <NumberInput.Root
      aria-label="Price"
      parser={(value) => value.replace(/[$,]/g, "")}
      formatter={(value) => `$${value}`}
    />
  );
}

The package also exports clampNumberValue, formatNumber, parseNumber, roundToPrecision, and stepNumberValue for consumers that need the same numeric calculations outside the rendered component.

Accessibility

The visible spinbutton owns native required validity; the named hidden input remains submission-only. A validation attempt is mirrored to Root and the spinbutton. Inline behavior suppresses only the browser bubble.

NumberInput follows the WAI-ARIA spinbutton pattern. The inner input renders role="spinbutton".

  • Atom owns aria-valuenow, aria-valuemin, aria-valuemax, aria-valuetext, aria-required, aria-readonly, and aria-invalid.
  • Provide an accessible name through native ARIA or Field. The visible spinbutton participates in external-form validity; the hidden value input submits the parsed number. Uncontrolled state resets to defaultValue.
KeyDescription
ArrowUpIncrements by step.
ArrowDownDecrements by step.
PageUpIncrements by largeStep.
PageDownDecrements by largeStep.
HomeMoves to min when provided.
EndMoves to max when provided.

Changelog

Unreleased

0.24.0

  • Added public Agent Knowledge for component selection, required composition, recurring mistakes, and validation.

0.19.0

  • Added compound Input, Increment, and Decrement parts while preserving the no-children and render-callback Root APIs.

0.6.16

  • Explicitly scrolled inline validation-directed focus into view.

0.6.15

  • Exposed inline validation-directed focus through [data-focus-visible] until blur.

0.6.13

  • Added inline/native validation presentation and native numeric-invalid reporting to the visible spinbutton, Field, and Form.

0.5.0

  • Added complete Field integration, native ARIA prop names, external-form validity association, and uncontrolled reset behavior.

0.1.0

  • Initial Atom release with spinbutton semantics, keyboard stepping, formatting, parsing, and hidden form input.