Skip to content

Locale-aware number input

NumberInput is the standalone locale-aware numeric control. It keeps transient text separate from the controlled number | null value, so clearing or typing an incomplete number never fabricates 0 or exposes NaN.

Use the native command NumberField when browser-native formatting and a non-null 0 default are sufficient. Use NumberInput or NumberInputField when the interaction needs locale grouping and decimal separators, fraction policy, adornments, nullable edit state, or explicit commit timing.

import { useState } from 'react';
import { NumberInput } from '@cratis/components/Common';
export const SampleQuantity = () => {
const [quantity, setQuantity] = useState<number | null>(null);
return (
<>
<label id='sample-quantity-label' htmlFor='sample-quantity'>
Quantity
</label>
<NumberInput
id='sample-quantity'
aria-labelledby='sample-quantity-label'
name='quantity'
value={quantity}
onChange={setQuantity}
min={0}
max={100}
step={0.5}
suffix='kg'
minimumFractionDigits={1}
maximumFractionDigits={2}
description='Enter a value from zero to one hundred.'
/>
</>
);
};

The nearest CratisComponentsProvider supplies the BCP 47 locale. The optional locale prop overrides it for one control. An invalid override falls back to the provider locale.

<CratisComponentsProvider value={{ locale: 'nb-NO' }}>
<NumberInput value={1234.5} onChange={setValue} aria-label='Amount' />
<NumberInput
value={1234.5}
onChange={setValue}
aria-label='American amount'
locale='en-US'
/>
</CratisComponentsProvider>

useGrouping defaults to true. When fraction props are omitted, decimal formatting uses the locale defaults: zero minimum fraction digits and up to three maximum fraction digits. Set both to the same number for fixed precision.

NumberInput deliberately separates editable text from semantic callbacks.

InteractiononChangeonCommit
Type or clear without leaving the inputNo callback while text is being editedNo callback
Press EnterNew finite number or null, when it changedThen Enter
Press Tab or otherwise blurNew finite number or null, when it changedThen Blur
Replace the complete input through pasteNew finite number or null, when it changedThen Paste
ArrowUp, ArrowDown, decrement, or incrementStepped finite numberThen Step
Commit text that cannot yet form a numberNo fabricated change; text returns to the controlled valueCurrent controlled value and the commit reason

A commit clamps to min/max, snaps to step, and rounds through the configured fraction policy. The same policy applies when a controlled prop arrives outside those boundaries: onChange receives the normalized value once. Until the owner accepts it, the control keeps the original value visible and withholds the named hidden input, so React state, announced content, and native form data cannot silently disagree. When both callbacks run, onChange always runs first. The component remains controlled: accept the value in onChange to display and submit it as the new value.

prefix and suffix render beside the editable text. They never enter the parse buffer, semantic number, or hidden form value. Each rendered adornment receives a stable id and is appended to the input’s aria-describedby relationship, together with consumer descriptions and an active error message.

Provide an accessible name through aria-label or aria-labelledby. When a visible external label is used, give it an id, keep htmlFor pointed at the input id, and pass that label id through aria-labelledby; this also ties the localized increment and decrement action names to the field. description and an invalid errorMessage are rendered and associated automatically. The editable text control retains React Aria’s number-field role description rather than reinstating the spinbutton attributes that its accessibility implementation intentionally removes for focus compatibility. Step buttons are excluded from sequential tab order; ArrowUp and ArrowDown provide the same operation from the input.

disabled removes the control from editing and form submission. readOnly keeps the value focusable and submittable while disabling edits and steps.

PropTypeDefaultBehavior
valuenumber | nullRequiredControlled finite value; non-finite runtime values render empty.
onChange(value: number | null) => voidRequiredReceives accepted semantic changes only.
onCommit(value, reason) => voidReceives Blur, Enter, Paste, or Step after the change callback.
localestringProvider localeBCP 47 locale override.
useGroupingbooleantrueEnables the locale grouping separator.
minimumFractionDigitsnumberLocale defaultMinimum displayed fraction digits.
maximumFractionDigitsnumberLocale defaultMaximum fraction digits retained on commit and shown in the formatted value.
requiredbooleanfalseRequires a non-empty value using native form and accessibility semantics.
min / maxnumberUnboundedCommit boundaries and number-field range.
stepnumber1Step and commit-snap interval.
showSteppersbooleantrueRenders the decrement/increment buttons; keyboard stepping works either way.
prefix / suffixReactNodeAssociated presentation outside the numeric value.
placeholderstringEmpty edit hint.
disabled / readOnly / invalidbooleanfalseSemantic and visual state.
id / namestringGenerated / —Label association and native form field name.
description / errorMessageReactNodeAssociated help and invalid-state content.
ptNumberInputPartsRenderer-independent part classes, styles, titles, and data attributes.
Typed pt key / DOM partMeaningCanonical states
rootComplete fielddisabled, invalid, readonly
groupThe bordered box holding adornments, input and steppersdisabled, invalid, readonly
inputEditable localized textdisabled, invalid, readonly, focused
prefix / suffixPresent adornmentdisabled, invalid, readonly
stepBoth step buttons; inspect data-step='decrement' or 'increment'disabled, invalid, readonly
descriptionSupporting textnone
errorActive validation messageinvalid

The component uses the shared control, surface, text, focus, disabled, and error tokens plus these aliases:

  • --cratis-number-input-adornment-color
  • --cratis-number-input-step-background
  • --cratis-number-input-step-background-hover

Use parts and tokens rather than internal element order or implementation-library selectors.

The built-in box — border, radius, background, shadow, focus ring and minimum height — is drawn on group, not on root. A product that draws its own box on root removes the built-in one through pt.group (and hides the buttons with showSteppers={false} when its field has no stepper affordance) instead of targeting .cratis-number-input__group. For a Tailwind consumer to override a component rule with a pt utility, the host’s @layer prelude must rank the cratis-* layers below its own utilities; see the cascade contract.