@k2b/ui
NumberInput
Bounded and formatted numeric input with steppers and clear state.
Accessor-controlled numeric input with raw focused text, committed bounds, precision, steppers, units, and an explicit empty state.
TSX
Copy<NumberInput label="Budget" value={budget} onValueChange={setBudget} prefix="€" suffix="gross" decimalPlaces={2} min={0} step={0.5} clearable/><NumberInput label="Capacity" value={capacity} onValueChange={setCapacity} suffix="%" min={0} max={100} step={5} /><NumberInput label="Workers" value={count} onValueChange={setCount} min={1} max={64} step={1} />NumberInput is an accessor-controlled numeric field with an explicit empty
state. The parent owns the number and decides when to persist it.
Use NumberInput
Use it for counts, limits, percentages, currency amounts, and other numeric form values.
Use TextInput when the value is an identifier that only looks numeric, such as an account number or postal code.
Import
import { NumberInput } from "@k2b/ui";Value and events
Pass value directly or as an accessor returning number | null. null and
an omitted value render as empty; callbacks emit null when the field is
cleared.
onValueChange receives the parsed value while the user types.
onValueCommit receives the normalized value after blur, a stepper click, or
the default clear action.
The component keeps the raw text while focused, so intermediate input is not lost. A trailing decimal separator survives only when the effective decimalPlaces is greater than 0; with an integer step a typed 12. is normalized to 12.
Numeric rules
decimalPlacesdefaults to the fraction digits ofstep(step={0.01}accepts two decimals, the defaultstep={1}accepts none). Set it explicitly when the accepted precision differs from the step grid; an explicit value always wins.- The visible decimal separator follows the effective locale (
localeprop, thenLocaleProvider, then<html lang>, then"en"); both comma and dot are accepted while typing. The controlled value stays a canonical JavaScript number andaria-valuenowstays numeric. - Editable text never shows grouping separators.
allowNegativedefaults totrue.minandmaxclamp committed values.stepdefaults to1and snaps committed values to its grid.- The default stepper buttons are integrated into the left and right edges of the input.
showSteppers={false}hides the buttons without disabling typed input.disableSteppersdisables only the buttons.- The placeholder and numeric value are both right-aligned; the placeholder is visually quieter than entered values.
prefix and suffix display short units inside the field. clearable adds an explicit empty-state action.
Edit a number in a property row
Set appearance="plain" when the number is a property shown in a
DescriptionList row, such as an estimate. The
value reads as text in the row's type, followed by its suffix, and the field
is exactly as wide as that text. There are no steppers or clear button: a click
anywhere in the row edits the value, Enter or leaving the field commits, Escape
restores the current value, and emptying the text clears it. Enter and Escape
keep focus in the field. The suffix hides while the field is empty, so the
placeholder reads alone. Commit through onValueCommit; leaving the field
fires it also when the value did not change, but not again right after Enter.
Pass formatValue to show a set value in a friendlier form while the field is
not being edited, such as 90 minutes as 1 h 30 min. Editing shows the
plain number with its suffix; at rest the formatted text stands alone.
On touch devices the field edits at 16 px like every other input, so iOS does not zoom. At rest the value keeps the row's size.
<NumberInput
aria-label="Estimate"
appearance="plain"
placeholder="No estimate"
suffix="min"
min={1}
value={estimate}
formatValue={formatDuration}
onValueCommit={saveEstimate}
/>;API reference
See shared field props for FieldProps, ValueFieldProps<T> and MaybeAccessor<T>.
type NumberInputProps = Omit<
JSX.InputHTMLAttributes<HTMLInputElement>,
"max" | "min" | "onChange" | "onInput" | "prefix" | "step" | "type" | "value" | keyof ValueFieldProps<number | null>
> &
ValueFieldProps<number | null> & {
max?: number;
min?: number;
step?: number;
decimalPlaces?: number;
allowNegative?: boolean;
clearable?: boolean;
onClear?: () => void;
clearLabel?: string;
increaseLabel?: string;
decreaseLabel?: string;
locale?: string;
showSteppers?: boolean;
disableSteppers?: boolean;
icon?: string;
activeIcon?: string;
prefix?: JSX.Element;
suffix?: JSX.Element;
appearance?: ChoiceAppearance;
formatValue?: (value: number) => string;
};appearance defaults to "field"; with "plain", showSteppers defaults to false. formatValue only changes the resting text of a plain field. onClear overrides the built-in reset to null; the host must then report its own value/commit update. icon and activeIcon replace the idle and focused icons.
Accessibility
Prefer a visible label. Without one, the placeholder becomes the accessible name, with Enter number as the final fallback.
The input exposes spinbutton semantics and finite minimum, maximum, and current values. Its value text includes a string suffix, such as 45 min, or the formatValue text; an explicit aria-valuetext wins. The stepper and clear controls have accessible names; override them per instance with increaseLabel, decreaseLabel, and clearLabel when the surrounding product is not English. Descriptions and reactive errors are connected to the field.
Runtime
The field renders in server HTML. Input filtering, raw-text preservation, steppers, clearing, and callbacks require browser code.
Example
const [price, setPrice] = createSignal<number | null>(12.5);
<NumberInput
label="Price"
value={price}
onValueChange={setPrice}
min={0}
step={0.01}
decimalPlaces={2}
suffix="€"
clearable
showSteppers={false}
/>;