NumberField
Numeric form input with stacked, split, inline or no increment/decrement buttons. Five sizes, five rounded values, full intent palette, three label positions, prefix/suffix slots, locale-aware formatting via Intl.NumberFormat, and LTR/RTL support.
Playground
Installation
pnpm add @tessinaui/uiUsage
import { NumberField } from "@tessinaui/ui";{/* Basic uncontrolled */}
<NumberField defaultValue={0} />
{/* Controlled */}
<NumberField value={qty} onValueChange={setQty} min={0} max={99} />
{/* Quantity selector with split buttons */}
<NumberField buttonsPlacement="split" defaultValue={1} min={1} max={10} />
{/* Currency input with locale formatting */}
<NumberField
defaultValue={1299.99}
step={0.01}
format={{ style: "currency", currency: "USD" }}
locale="en-US"
/>
{/* Percent input */}
<NumberField
defaultValue={0.245}
step={0.001}
format={{ style: "percent", minimumFractionDigits: 1 }}
/>
{/* With label + supporting text + intent */}
<NumberField
label="Quantity"
intent="error"
supportingText="Out of stock"
required
defaultValue={0}
/>
{/* Prefix / suffix slots */}
<NumberField
defaultValue={75}
suffix={<span className="px-3 py-2 bg-secondary rounded-full">kg</span>}
/>
{/* RTL */}
<NumberField dir="rtl" label="الكمية" defaultValue={1} />Showcase
NumberField vs Stepper
The two components solve different problems:
| NumberField | Stepper | |
|---|---|---|
| Purpose | Form input — user types or steps a value | Compact counter — user picks a value with buttons |
| Input | Editable text field with keyboard / paste support | No input; value is read-only display |
| Affordances | Label, supporting text, prefix/suffix, error states | Label + supporting text only |
| Typical use | "Price", "Age", "Weight" form fields | Quantity selector in a cart row |
If users can type the value, use NumberField. If they always pick from a small range with +/-, use Stepper.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | null | — | Controlled value |
defaultValue | number | — | Uncontrolled initial value |
min | number | — | Minimum value (decrement disabled at min) |
max | number | — | Maximum value (increment disabled at max) |
step | number | "any" | 1 | Amount per increment / decrement |
smallStep | number | 0.1 | Step when Meta key is held |
largeStep | number | 10 | Step when Shift key is held |
snapOnStep | boolean | false | Snap to nearest step on increment / decrement |
format | Intl.NumberFormatOptions | — | Locale-aware formatting (currency, percent, compact, decimals) |
locale | Intl.LocalesArgument | runtime | BCP-47 locale tag |
allowWheelScrub | boolean | false | Scrub the value with the mouse wheel while focused |
allowOutOfRange | boolean | false | Direct text entry can exceed min / max for native validation |
onValueChange | (value, eventDetails) => void | — | Called whenever the value changes |
onValueCommitted | (value, eventDetails) => void | — | Called on blur / pointer release / keyboard commit |
name | string | — | HTML form name |
required | boolean | false | Mark required (asterisk in label, native validation) |
disabled | boolean | false | Disable all interaction |
readOnly | boolean | false | Value visible but not editable |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Field size |
rounded | "none" | "sm" | "md" | "lg" | "full" | "full" | Container border radius |
intent | "none" | "error" | "warning" | "success" | "info" | "none" | Border colour and supporting-text colour |
label | string | — | Visible label |
labelPosition | "outside-top" | "inside" | "outside-left" | "outside-top" | Where the label sits |
infoText | string | — | Small info text rendered next to the label |
supportingText | string | — | Helper / validation text below the field, coloured by intent |
prefix | ReactNode | — | Content rendered outside the field on the leading edge |
suffix | ReactNode | — | Content rendered outside the field on the trailing edge |
placeholder | string | — | Input placeholder |
buttonsPlacement | "stacked" | "split" | "inline" | "none" | "stacked" | Layout of the increment / decrement buttons |
incrementLabel | string | "Increase" | aria-label for the + button |
decrementLabel | string | "Decrease" | aria-label for the − button |
dir | "ltr" | "rtl" | inherited | Text direction |
id | string | auto | id of the underlying input |
inputRef | Ref<HTMLInputElement> | — | Ref to the underlying input element |
inputMode | string | auto | Mobile keyboard hint — numeric for integers, decimal otherwise |
wrapperClassName | string | — | Class on the outermost wrapper |
containerClassName | string | — | Class on the bordered container |
className | string | — | Class on the <input> itself |
Buttons placement
| Placement | Description |
|---|---|
stacked | Two stacked chevrons on the trailing edge with a vertical divider. Each button is half the input height. |
split | − on the leading edge, + on the trailing edge. Each button is square and full-height. |
inline | − and + side-by-side on the trailing edge. Both square, both full-height. |
none | No visible buttons. Keyboard arrows and scroll wheel still work. |
Intents
Same palette as Field — affects border, focus ring colour, and supporting-text colour.
| Intent | Border | Focus ring | Supporting text |
|---|---|---|---|
none | border-border | ring-ring | text-muted-foreground |
error | border-error | ring-error | text-error |
warning | border-warning | ring-warning | text-warning |
success | border-success | ring-success | text-success |
info | border-info | ring-info | text-info |
Keyboard
| Key | Action |
|---|---|
↑ / ↓ | Increment / decrement by step |
Shift + ↑ / Shift + ↓ | Increment / decrement by largeStep |
Meta + ↑ / Meta + ↓ | Increment / decrement by smallStep |
Page Up / Page Down | Increment / decrement by largeStep |
Home / End | Jump to min / max (when set) |
Notes
- Controlled vs uncontrolled: pass
valuefor controlled,defaultValuefor uncontrolled. The value can benullwhen the input is empty. - Locale formatting: passing
formatruns the value throughIntl.NumberFormat— currencies, percentages, compact notation, and custom decimal places all work out of the box. - Scroll-wheel scrub: opt-in via
allowWheelScrub. When enabled, scrolling over a focused field changes the value. - RTL:
dir="rtl"is supported. Stacked / inline buttons stay on the trailing edge; split places+on the visual right and−on the visual left, matching standard RTL number-input conventions. - Accessibility: built on
@base-ui/react/number-field. The hidden input is a real<input type="number">so HTML form submission, native validation, and assistive technologies behave correctly. - Form submission: pass
nameto include the value in standard form submissions.
Textarea
Multi-line text input with intent variants, sizes, label positions, character counter, auto-resize, and LTR/RTL support.
Date Picker
A button-triggered date picker popover built on Calendar and Base UI Popover. Supports single, range, and multiple selection, preset shortcuts, stacked or sidebar preset layouts, footer slot, and min/max constraints.