Noctis

NumberField

Enter a number with confidence. A bordered field shell pairs a numeric input with steppers, accepts keyboard arrows and pointer scrubbing, and formats the value — currency, percent, units, precision — for the reader's locale.

Basic

Compose the shell from its parts: a NumberField.Decrement, the NumberField.Input, and a NumberField.Increment inside one NumberField.Group. The steppers are ghost icon buttons that increment and decrement by the field's step; the input takes typed entry and clamps on blur. By default the steppers sit as a horizontal −/+ pair grouped at the inline end (steppers="end"). Give the input an aria-label (or wire it to a visible label) so it is named for assistive tech.

Formatting

The reason a number field beats a plain text input: pass Base UI's format (the full Intl.NumberFormat options) and the value displays as currency, a percent, a unit, or to a fixed precision — grouped and localized — while the committed value stays a plain number. Percent treats the value as a fraction (0.2 shows as 20%), so step by 0.01 to move 1% at a time.

Affixes

For a unit Intl can't render — px, an ad-hoc symbol — seat a NumberField.Prefix or NumberField.Suffix inside the shell. They are segmented unit-hint cells mirroring Input's addon adornment — a quiet fill stepped off the field surface with a divider to the value — and are visual chrome only: non-interactive, never changing the committed value. Reach for format when the unit is a real currency or CLDR unit (so the value formats and localizes); reach for an affix when it is just a hint beside a plain number.

$
px

Steppers

steppers sets where the increment/decrement buttons sit. end (the default) groups them as a horizontal −/+ pair at the inline end; split flanks the value; stacked blocks them in a sharp column at the inline end; none drops the buttons entirely — arrow keys and scrubbing still step the value, for dense toolbars where chrome is noise.

Sizes

Two heights — medium (the default) and large — sharing the control rhythm. The steppers and type scale with the shell, so the buttons stay a comfortable target at both sizes.

Constraints

Bound the value with min and max, and set the increment with step. The steppers and arrow keys move by step and clamp at the bounds; at a bound the stepper that can no longer move greys out, so the limit is felt. Hold Shift for the large step, the meta key for the small step.

Inside a field

The canonical usage — and the pattern the whole field family shares. Wrap the field in a Noctis Field.Root with a Field.Label, Field.Description, and Field.Error: the label auto-associates, the description and error join the input through aria-describedby, and aria-invalid flips from validation — driving the shell's danger border, which wins over the focus border when both apply. Put bounds and step context in the Field.Description (not the placeholder).

How many seats to reserve (1–10).

Scrubbing

Wrap a label or handle in NumberField.ScrubArea to let the reader drag horizontally to change the value — the pointer gesture familiar from design tools. pixelSensitivity tunes how far the pointer travels per step; a NumberField.ScrubAreaCursor inside the area becomes a virtual cursor that follows the pointer 1:1 during the drag. The input and steppers stay available for precise entry.

Width

Keyboard

The input owns the keyboard; the steppers are pointer targets and stay out of the tab order. All directions are absolute — Up always increases — so they do not mirror under RTL.

KeyAction
Arrow UpIncrease by step
Arrow DownDecrease by step
Shift + Arrow Up / Shift + Arrow DownStep by largeStep (default 10)
Meta + Arrow Up / Meta + Arrow DownStep by smallStep (default 0.1)
Page Up / Page DownStep by largeStep
Home / EndJump to min / max (when bounded)

Accessibility

  • Role. The input keeps Base UI's role="textbox" with aria-roledescription="Number field", rather than role="spinbutton". This is deliberate: a textbox formats and accepts free-form locale-aware entry (Persian numerals, grouping, currency) that the spinbutton model fights, and the formatted value is reflected as the input's value, so a screen reader reads it on focus and after each step. aria-valuenow/valuemin/valuemax are spinbutton/slider properties — invalid on a textbox — so they are intentionally omitted; convey bounds and step in a Field.Description instead.
  • Steppers. The increment/decrement buttons keep accessible names (the translated Increase / Decrease) but stay out of the tab order (tabindex="-1"), since the arrow keys mirror them and focus never leaves the input.
  • At a bound. The stepper that can no longer move is greyed and aria-disabled, so the limit is perceivable, not silent.
  • Mobile. The input requests the right soft keyboard automatically — inputMode="decimal" when the field admits fractions (a fractional step, a percent, or a format with fraction digits), else inputMode="numeric".
  • RTL. Directions are absolute (Up always increases) and never mirror; the shell, affixes, and stepper block flip sides logically.

Anatomy

Compose the field from its parts. NumberField.Root owns the value (controlled via value / onValueChange, or uncontrolled via defaultValue), the shared size, and the steppers layout.

  • NumberField.Root — the container and value owner. Props: size (default md), steppers (default end), plus the Base UI NumberField.Root props below.
  • NumberField.Group — the bordered field shell. Wraps the affixes, input, and steppers, carries the focus border, and stamps data-steppers.
  • NumberField.Input — the numeric input. Give it an aria-label (or an associated Field.Label) so it is named; it derives its inputMode from format/step.
  • NumberField.Prefix / NumberField.Suffix — optional leading/trailing unit-hint affixes (muted, non-interactive). Visual chrome only.
  • NumberField.Decrement / NumberField.Increment — the down and up steppers, ghost icon buttons. Pass children to swap the default minus/plus glyph; the accessible name is the translated Decrease / Increase label.
  • NumberField.ScrubArea — an optional drag handle that scrubs the value by horizontal pointer movement (direction, pixelSensitivity).
  • NumberField.ScrubAreaCursor — the virtual cursor shown inside the scrub area during a drag.

Base UI pass-through props (set on NumberField.Root, forwarded as-is): min / max (bounds), step (arrow/stepper increment), smallStep (meta key, default 0.1), largeStep (shift / Page, default 10), snapOnStep (snap to the nearest step multiple), format (Intl.NumberFormat options), locale (overrides the injected locale), allowWheelScrub (opt-in wheel-to-change), allowOutOfRange (permit values past the bounds), and onValueCommitted (fires on blur/release, not every keystroke).

Every part carries a data-slot (number-field, number-field-group, number-field-input, number-field-prefix, number-field-suffix, number-field-increment, number-field-decrement, number-field-scrub-area, number-field-scrub-area-cursor) for host-side styling — pair it with the state attributes (data-size, data-steppers, data-disabled, data-readonly, data-scrubbing, and the data-valid / data-invalid / data-focused a wrapping Field.Root adds). The field is RTL-aware and respects prefers-reduced-motion.

On surfaces

The same control re-tuned across the elevation scopes — the root canvas, an elevated panel, a menu, and a sunken well. It stays legible on every layer.

root
elevated
menu
sunken

Design tokens

Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every number field in that region retunes — e.g. .dense { --noctis-number-field-stepper-width: var(--noctis-size-control-xs); } narrows the steppers. Knobs that aren't minted are reached through the part's data-slot. See Customization for the full override ladder and Tokens for the whole graph.

Token

API reference

Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table; parts that only forward to Base UI list just the props they pass through. Expand a row for the full type and description.

NumberField.Root

Prop

NumberField.Group

Prop

NumberField.Input

Prop

NumberField.Prefix

Prop

NumberField.Suffix

Prop

NumberField.Decrement

Prop

NumberField.Increment

Prop

NumberField.ScrubArea

Prop

NumberField.ScrubAreaCursor

Prop

AttributeDescription
data-slotThe slot marking a rendered element as a NumberField part.
data-sizeThe size scale — `md` | `lg`; the generated layer keys the per-size internals off it.
data-steppersThe stepper layout — `end` (default) | `split` | `stacked` | `none`; stamped on the group, the CSS branches off it.
data-scrubbingPresent while the value is being scrubbed by pointer drag.
data-disabledPresent when the number field is disabled.
data-readonlyPresent when the number field is read-only.
data-requiredPresent when the number field is required.
data-validPresent when the number field is valid (when wrapped in `Field.Root`).
data-invalidPresent when the number field is invalid (when wrapped in `Field.Root`).
data-focusedPresent when the number field is focused (when wrapped in `Field.Root`).