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} />Examples
Default
A labelled numeric input with stacked stepper buttons.
Buttons placement
Step buttons can be stacked, split, inline, or hidden with none.
Sizes
Five sizes from xs to xl.
Intents
Colour the border, focus ring, and supporting text with error, warning, success, or info.
Label positions
Place the label outside-top, inside, or outside-left.
Locale-aware formatting
Pass format to render currency, percent, or compact values via Intl.NumberFormat.
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.
Navigation Menu
A full top-bar navigation pattern — logo, centre menu with mega-menu popups, and right-aligned action slots for language selector and auth CTAs. Ships two desktop trigger treatments (`pill`, `underline`), a `mode="mobile"` variant with a right-side drawer + submenu drill-in, richer mega-menu rows (tag + badge), and a theme-aware logo that swaps between light and dark sources. Built on @base-ui/react with 5 sizes, 5 rounded options, and LTR/RTL support.
OTP Input
One-time password input with individual digit cells. Supports three visual variants, five sizes, five intents, masking, separator groups, numeric/alphanumeric/alphabetic input types, and full RTL/LTR support.