Number Field
A compact numeric field with a leading Minus and trailing Plus stepper button — keyboard stepping, thousands separators and a border by default.
Installation
Usage
NumberField is the stepper-style numeric field: a leading Minus and trailing Plus button step the value by step (clamped to min / max). The field ships with a border by default; set variant="embedded" when it lives inside another container (toolbars, table cells, custom shells) and the border is dropped. Each stepper button disables itself once the value reaches its side's bound. Keyboard stepping works while the input is focused.
"use client";import { useState } from "react";import { NumberField } from "@/ui";export default function Demo() { const [count, setCount] = useState<number | undefined>(1); return ( <div className="flex w-full max-w-md flex-wrap items-start gap-6"> <div className="flex flex-col gap-1"> <span className="text-xs text-muted-foreground">Quantity · 1–99</span> <NumberField positiveInteger min={1} max={99} value={count} onValueChange={setCount} aria-label="Quantity" /> </div> <div className="flex flex-col gap-1"> <span className="text-xs text-muted-foreground">Rate · 0–1, step 0.1</span> <NumberField min={0} max={1} step={0.1} defaultValue={0.5} aria-label="Rate" /> </div> <div className="flex flex-col gap-1"> <span className="text-xs text-muted-foreground">Embedded · no border</span> <NumberField variant="embedded" positiveInteger min={1} max={5} defaultValue={1} aria-label="Embedded quantity" /> </div> <div className="flex flex-col gap-1"> <span className="text-xs text-muted-foreground">Disabled</span> <NumberField defaultValue={7} disabled aria-label="Disabled" /> </div> </div> );}Controlled
Normal Mode (string value)
Value: 50 (type: string)
Positive Integer Mode (number | null value)
Value: 5 (type: number)
"use client";
import * as React from "react";
import { NumberField } from "@/ui";
export default function NumberFieldControlledDemo() {
const [stringValue, setStringValue] = React.useState<string | undefined>(
"50",
);
const [numberValue, setNumberValue] = React.useState<number | undefined>(5);
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">Normal Mode (string value)</p>
<NumberField
value={stringValue}
onValueChange={setStringValue}
min={0}
max={100}
step={0.5}
aria-label="String value"
/>
<p className="text-sm text-muted-foreground">
Value: {stringValue} (type: {typeof stringValue})
</p>
</div>
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">
Positive Integer Mode (number | null value)
</p>
<NumberField
value={numberValue}
onValueChange={setNumberValue}
positiveInteger
min={0}
max={20}
aria-label="Integer value"
/>
<p className="text-sm text-muted-foreground">
Value: {numberValue} (type:{" "}
{numberValue === null ? "null" : typeof numberValue})
</p>
</div>
</div>
);
}
API Reference
NumberField
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | number | string | - | Initial value (uncontrolled mode) |
value | number | string | undefined | - | Current value (controlled mode) |
onValueChange | (value?: number) => void | (value?: string) => void | - | Value change handler. Returns number | undefined when positiveInteger=true, otherwise string |
invalid | boolean | - | Switches the border (or the text in embedded mode) to danger |
variant | "default" | "embedded" | "default" | "default" ships a border for standalone use; "embedded" drops it for fields inside other containers |
min | number | 0 (positiveInteger) or -Infinity | Minimum allowed value — the Minus button disables at it |
max | number | Infinity | Maximum allowed value — the Plus button disables at it |
step | number | 1 | Step size for the steppers and ArrowUp / ArrowDown |
positiveInteger | boolean | false | Restrict to positive integers only. Changes value type to number | undefined |
thousands | boolean | false | Group the integer part with thousands separators while the input is not focused; the committed value stays unformatted |
className | ClassNameValue | - | Custom classes, applied to the shell |
style | React.CSSProperties | - | Inline styles, applied to the shell |
classNames | { root? } | - | Custom classes for the inner input |
styles | { root? } | - | Inline styles for the inner input |
disabled | boolean | false | Disable the field and both steppers |
...props | React.ComponentProps<"input"> | - | Supports all native input props (except type), such as onBlur |
Composable Components
NumberStepper
The clickable step button — renders a Plus (direction="up") or Minus (direction="down") icon; children overrides the icon.
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "up" | "down" | "up" | Icon and accessible label (Increase / Decrease) |
className | ClassNameValue | - | Custom CSS class |
children | React.ReactNode | - | Custom icon content, replaces the default icon |
Supports all native <button> attributes (except type).
NumberRoot
The bare inner input. Supports all native <input> attributes (except className), plus className.
For the bordered variant with a non-interactive suffix cue, see NumberInput.
Multi Select
A multi-select dropdown composed of Popover and CheckboxGroup — flat or grouped options, keyboard navigation, controlled value
Number Input
A bordered numeric input with a non-interactive up/down cue in the trailing area — stepping is keyboard-only. The form-friendly sibling of NumberField.