Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Number Field

Locale-aware numeric input on Base UI's NumberField in Input's field chrome — Intl formatting, min/max/step, keyboard stepping, wheel scrub.

Status
stable
Since
0.4.0
Accessibility pattern
APG spinbutton

Last updated

Quantity

Install

Add Number Field from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/number-field

Usage

import { NumberField } from "@/components/ui/number-field";

<NumberField aria-label="Quantity" defaultValue={2} min={0} max={99} />;

NumberField wraps Base UI's NumberField: locale-aware parsing and formatting (format: Intl.NumberFormatOptions + locale), min/max/step with snapOnStep, keyboard stepping, and wheel scrubbing — rendered in Input's exact addon-group chrome with full-height − / + steppers.

Like Input, the size prop is the control-height variant on the shared 28/32/40 scale and replaces the native numeric size attribute.

What lives elsewhere, and why:

BehaviourWhere it lives
Minor-units (cents) handlingYour field layer — the component works in display units
Pointer scrubbing (ScrubArea)Compose BaseNumberField.ScrubArea yourself — no keyboard/touch parity
Labels, descriptions, errorsField around the control
Free-text digits (phone, ids)Input — formatting semantics would fight them

Examples

Money

Money is a format prop, not a separate component: format={{ style: "currency", currency: "USD" }}. The currency-code Select drops into the suffix slot.

Deal amount

Money is a format prop — the currency Select sits in the suffix slot.

One recipe belongs to your field layer, not this component: minor units. If your API stores integer cents, convert at the boundary — the component works in display units:

<NumberField
  aria-label="Amount"
  value={amountInCents / 100}
  onValueChange={(v) => setAmountInCents(Math.round((v ?? 0) * 100))}
  format={{ style: "currency", currency: "USD" }}
/>

Sizes, units, and states

Every size on the shared 28/32/40 scale, with the unit suffix, hideControls, disabled, and the invalid border tint. Wrap the field in a Field to pair the tint with a message and the invalid shake.

kg

API Reference

PropTypeDefaultDescription
aria-labelstringAccessible name for the numeric input. Required in practice unless a wrapping Field/aria-labelledby supplies one — the input must never be unnamed.
classNamestringExtra classes for the bordered group root.
hideControlsbooleanfalseHide the − / + stepper buttons. Keyboard stepping (arrows, Home/End) and wheel scrub keep working — the buttons are a pointer affordance only.
inputClassNamestringClasses for the inner <input> element (e.g. text-end for columnar numbers).
inputRefReact.Ref<HTMLInputElement>Ref forwarded to the inner <input> element.
placeholderstringPlaceholder for the empty input.
prefixReact.ReactNodeNon-editable addon before the input (a unit, an icon, a currency code) — Input's addon idiom. Plain strings render as muted, non-selectable text.
size"lg" | "md" | "sm"'md'Control height on the shared 28/32/40 scale (--size-sm/md/lg), matching Input/Button/Select. (The native numeric size attribute is intentionally replaced by this variant prop, exactly as on Input.)
suffixReact.ReactNodeNon-editable addon after the input. The documented seat for a currency-code Select in the money recipe.

Data attributes and CSS variables on NumberField

AttributeValues
data-field-group""
data-sizemirrors a prop or state value
data-slot"number-field" | "number-field-decrement" | "number-field-increment" | "number-field-input" | "number-field-prefix" | "number-field-suffix"

All Base UI NumberField.Root props pass through: format, locale, min, max, allowOutOfRange, step / largeStep / smallStep, snapOnStep, allowWheelScrub, readOnly, required, onValueChange, onValueCommitted, and native form integration via name.

Accessibility

  • The inner input must carry an accessible name — pass aria-label (or label it through a wrapping Field).
  • The − / + steppers are full-height, ≥24px pointer targets with accessible names ("Decrease" / "Increase"); keyboard users step with the arrow keys instead, so the buttons add no tab stops beyond Base UI's defaults.
  • Focus uses the text-entry affordance: the group border tints on focus-within, matching Input's addon mode.
KeyAction
/ Step by step.
Shift + /Step by largeStep.
Alt + /Step by smallStep.
Home / EndJump to min / max when set.
ContractStates tested
Behaviourdefault, stepping, clamped, disabled, read-only, invalid
Accessibilitykeyboard, labeled, semantic-html
Visualdefault, focus, invalid, disabled, dark

Do / Don't

Do
Express money, percents, and units through the Intl format prop — one numeric input, locale-correct everywhere.
Don't
Build a separate money-input or hand-parse currency strings out of a text Input — parsing is exactly what this component owns.

On this page