gnome-ui
Components

Number Field

An input specifically designed for numbers, complete with increment/decrement steppers, scrub area support, and advanced formatting.

Number Field component
import * as React from 'react';
import { NumberField } from 'gnome-ui/number-field';
import { ChevronUp, ChevronDown } from 'lucide-react';

export function NumberFieldDefault() {
  return (
    <NumberField.Root defaultValue={10} className="flex flex-col gap-1.5 w-32">
      <NumberField.Group className="flex h-10 w-full overflow-hidden rounded-xl border border-input bg-card text-foreground shadow-sm transition-colors focus-within:border-primary focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring">
        <NumberField.Input className="w-full bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground" />
        <div className="flex flex-col border-l border-input">
          <NumberField.Increment className="flex flex-1 items-center justify-center px-2 hover:bg-accent hover:text-accent-foreground disabled:opacity-50">
            <Icons.ChevronUp className="size-3" strokeWidth={3} />
          </NumberField.Increment>
          <NumberField.Decrement className="flex flex-1 items-center justify-center border-t border-input px-2 hover:bg-accent hover:text-accent-foreground disabled:opacity-50">
            <Icons.ChevronDown className="size-3" strokeWidth={3} />
          </NumberField.Decrement>
        </div>
      </NumberField.Group>
    </NumberField.Root>
  );
}

Anatomy

import { NumberField } from 'gnome-ui/number-field';

<NumberField.Root>
  <NumberField.ScrubArea>
    <NumberField.ScrubAreaCursor />
  </NumberField.ScrubArea>
  <NumberField.Group>
    <NumberField.Decrement />
    <NumberField.Input />
    <NumberField.Increment />
  </NumberField.Group>
</NumberField.Root>

Examples

Formatting

You can pass Intl.NumberFormatOptions to the format prop to automatically format the input value. The steppers can also be placed side-by-side for a more horizontal layout.

Number Field component
import * as React from 'react';
import { NumberField } from 'gnome-ui/number-field';
import { Minus, Plus } from 'lucide-react';

export function NumberFieldFormatting() {
  return (
    <NumberField.Root 
      defaultValue={99} 
      format={{ style: 'currency', currency: 'USD' }}
      className="flex flex-col gap-1.5 w-48"
    >
      <NumberField.Group className="flex h-10 w-full items-center overflow-hidden rounded-xl border border-input bg-card text-foreground shadow-sm transition-colors focus-within:border-primary focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring">
        <NumberField.Decrement className="flex h-full items-center justify-center border-r border-input px-3 hover:bg-accent hover:text-accent-foreground disabled:opacity-50">
          <Icons.Minus className="size-4" strokeWidth={2.5} />
        </NumberField.Decrement>
        <NumberField.Input className="w-full bg-transparent px-3 py-2 text-center text-sm outline-none placeholder:text-muted-foreground" />
        <NumberField.Increment className="flex h-full items-center justify-center border-l border-input px-3 hover:bg-accent hover:text-accent-foreground disabled:opacity-50">
          <Icons.Plus className="size-4" strokeWidth={2.5} />
        </NumberField.Increment>
      </NumberField.Group>
    </NumberField.Root>
  );
}

Scrub Area

The ScrubArea component provides a label that users can click and drag horizontally or vertically to change the value smoothly without typing.

Number Field component
import * as React from 'react';
import { NumberField } from 'gnome-ui/number-field';
import { ArrowsRightLeft } from 'lucide-react';

export function NumberFieldScrub() {
  return (
    <NumberField.Root defaultValue={50} className="flex flex-col gap-1.5 w-32">
      <NumberField.ScrubArea className="group flex w-fit cursor-ew-resize select-none items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
        <Icons.ArrowsRightLeft className="size-3.5" />
        <label className="cursor-ew-resize">Adjust</label>
        <NumberField.ScrubAreaCursor className="rounded-full bg-primary/20 backdrop-blur-sm" />
      </NumberField.ScrubArea>
      
      <NumberField.Group className="flex h-10 w-full overflow-hidden rounded-xl border border-input bg-card text-foreground shadow-sm transition-colors focus-within:border-primary focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring">
        <NumberField.Input className="w-full bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground" />
      </NumberField.Group>
    </NumberField.Root>
  );
}

NumberFieldDecrement

A stepper button that decreases the field value when clicked. Renders an &lt;button&gt; element.

Documentation: Base UI Number Field

Props

PropTypeDefaultDescription
classNamestring | (...) => string)CSS class applied to the element, or a function that
returns a class based on the component’s state.
renderReactElement> | ComponentRenderFnAllows you to replace the component’s HTML element
with a different tag, or compose it with another component.

Accepts a ReactElement or a function that returns the element to render.
nativeButtonbooleantrueWhether the component renders a native &lt;button&gt; element when replacing it
via the render prop.
Set to false if the rendered element is not a button (e.g. &lt;div&gt;).

NumberFieldGroup

Groups the input with the increment and decrement buttons. Renders a &lt;div&gt; element.

Documentation: Base UI Number Field

Props

PropTypeDefaultDescription
classNamestring | (...) => string)CSS class applied to the element, or a function that
returns a class based on the component’s state.
renderReactElement> | ComponentRenderFnAllows you to replace the component’s HTML element
with a different tag, or compose it with another component.

Accepts a ReactElement or a function that returns the element to render.

NumberFieldIncrement

A stepper button that increases the field value when clicked. Renders an &lt;button&gt; element.

Documentation: Base UI Number Field

Props

PropTypeDefaultDescription
classNamestring | (...) => string)CSS class applied to the element, or a function that
returns a class based on the component’s state.
renderReactElement> | ComponentRenderFnAllows you to replace the component’s HTML element
with a different tag, or compose it with another component.

Accepts a ReactElement or a function that returns the element to render.
nativeButtonbooleantrueWhether the component renders a native &lt;button&gt; element when replacing it
via the render prop.
Set to false if the rendered element is not a button (e.g. &lt;div&gt;).

NumberFieldInput

The native input control in the number field. Renders an &lt;input&gt; element.

Documentation: Base UI Number Field

Props

PropTypeDefaultDescription
classNamestring | (...) => string)CSS class applied to the element, or a function that
returns a class based on the component’s state.
aria-roledescriptionstring'Number field'A string value that provides a user-friendly name for the role of the input.
renderReactElement> | ComponentRenderFnAllows you to replace the component’s HTML element
with a different tag, or compose it with another component.

Accepts a ReactElement or a function that returns the element to render.

NumberFieldRoot

Groups all parts of the number field and manages its state. Renders a &lt;div&gt; element.

Documentation: Base UI Number Field

Props

PropTypeDefaultDescription
classNamestring | (...) => string)CSS class applied to the element, or a function that
returns a class based on the component’s state.
defaultValuenumberThe uncontrolled value of the field when it’s initially rendered.

To render a controlled number field, use the value prop instead.
idstringThe id of the input element.
renderReactElement> | ComponentRenderFnAllows you to replace the component’s HTML element
with a different tag, or compose it with another component.

Accepts a ReactElement or a function that returns the element to render.
valuenumberThe raw numeric value of the field.
disabledbooleanfalseWhether the component should ignore user interaction.
stepnumber | "any"1Amount to increment and decrement with the buttons and arrow keys, or to scrub with pointer movement in the scrub area.
To always enable step validation on form submission, specify the min prop explicitly in conjunction with this prop.
Specify step="any" to always disable step validation.
namestringIdentifies the field when a form is submitted.
requiredbooleanfalseWhether the user must enter a value before submitting a form.
readOnlybooleanfalseWhether the user should be unable to change the field value.
inputRefRef<HTMLInputElement>A ref to access the hidden input element.
localeLocalesArgumentThe locale of the input element.
Defaults to the user's runtime locale.
onValueChange(...) => voidCallback fired when the number value changes.

The eventDetails.reason indicates what triggered the change:
- 'input-change' for parseable typing or programmatic text updates
- 'input-clear' when the field becomes empty
- 'input-blur' when formatting (and clamping, if enabled) occurs on blur
- 'input-paste' for paste interactions
- 'keyboard' for keyboard input
- 'increment-press' / 'decrement-press' for button presses on the increment and decrement controls
- 'wheel' for wheel-based scrubbing
- 'scrub' for scrub area drags
maxnumberThe maximum value of the input element.
minnumberThe minimum value of the input element.
formatNumberFormatOptionsOptions to format the input value.
allowOutOfRangebooleanfalseWhen true, direct text entry may be outside the min/max range without clamping,
so native range underflow/overflow validation can occur.
Step-based interactions (keyboard arrows, buttons, wheel, scrub) still clamp.
smallStepnumber0.1The small step value of the input element when incrementing while the meta key is held. Snaps
to multiples of this value.
largeStepnumber10The large step value of the input element when incrementing while the shift key is held. Snaps
to multiples of this value.
allowWheelScrubbooleanfalseWhether to allow the user to scrub the input value with the mouse wheel while focused and
hovering over the input.
snapOnStepbooleanfalseWhether the value should snap to the nearest step when incrementing or decrementing.
onValueCommitted(...) => voidCallback function that is fired when the value is committed.
It runs later than onValueChange, when:
- The input is blurred after typing a value.
- The pointer is released after scrubbing or pressing the increment/decrement buttons.

It runs simultaneously with onValueChange when interacting with the keyboard.

Warning: This is a generic event not a change event.

NumberFieldScrubArea

An interactive area where the user can click and drag to change the field value. Renders a &lt;span&gt; element.

Documentation: Base UI Number Field

Props

PropTypeDefaultDescription
classNamestring | (...) => string)CSS class applied to the element, or a function that
returns a class based on the component’s state.
renderReactElement> | ComponentRenderFnAllows you to replace the component’s HTML element
with a different tag, or compose it with another component.

Accepts a ReactElement or a function that returns the element to render.
direction"horizontal" | "vertical"'horizontal'Cursor movement direction in the scrub area.
pixelSensitivitynumber2Determines how many pixels the cursor must move before the value changes.
A higher value will make scrubbing less sensitive.
teleportDistancenumberIf specified, determines the distance that the cursor may move from the center
of the scrub area before it will loop back around.

NumberFieldScrubAreaCursor

A custom element to display instead of the native cursor while using the scrub area. Renders a &lt;span&gt; element.

This component uses the Pointer Lock API, which may prompt the browser to display a related notification. It is disabled in Safari to avoid a layout shift that this notification causes there.

Documentation: Base UI Number Field

Props

PropTypeDefaultDescription
classNamestring | (...) => string)CSS class applied to the element, or a function that
returns a class based on the component’s state.
renderReactElement> | ComponentRenderFnAllows you to replace the component’s HTML element
with a different tag, or compose it with another component.

Accepts a ReactElement or a function that returns the element to render.

On this page