Inputs

Input Number

Numeric input with step controls, filtered typing, and separate visible and committed values for controlled or native forms.

Overview

InputNumber composes Input Group with a text field and suffix step buttons. It accepts string or number control, strips invalid characters while typing, and uses onChange for the visible text and onValueChange when a value commits (blur, , /, or the step buttons). Mouse-wheel scrolling over the field does not change the value.

Key bindings

KeyBehavior
Increases by step, clamped to min / max.
Decreases by step, clamped to min / max.
Commits the current text: parses, clamps, reformats, and fires onValueChange when valid.
blurCommits the same way as when focus leaves the input.

Custom props

PropTypeDescription
valuestring | numberControlled value. Strings control the visible text; numbers control the numeric state.
onValueChange(value: number | null) => voidFires when the committed numeric value changes (see Overview and Key bindings).
minnumberOptional minimum clamp. Ignored if min > max.
maxnumberOptional maximum clamp. Ignored if min > max.
stepnumberStep size for buttons and arrow keys. Invalid or non-positive values fall back to 1.
size'xs' | 'sm' | 'default' | 'lg'Optional. Passed to the root InputGroup: shell height matches other inputs; the text field and step buttons scale with the group.

Install

Configure your registry in components.json:

{
  "registries": {
    "@uxio": "https://ui.uxio.dev/r/styles/{style}/{name}.json"
  }
}

Then run:

npx shadcn@latest add @uxio/input-number

This pulls input-group as a registry dependency. The correct variant (Base UI or Radix) follows your components.json style.

Usage

String-controlled

import * as React from "react"

import { InputNumber } from "@/components/ui/input-number"

export function QuantityField() {
  const [text, setText] = React.useState("")
  const [committed, setCommitted] = React.useState<number | null>(null)

  return (
    <InputNumber
      value={text}
      min={0}
      max={10}
      step={0.5}
      onChange={(event) => setText(event.target.value)}
      onValueChange={setCommitted}
    />
  )
}

Number-controlled

import * as React from "react"

import { InputNumber } from "@/components/ui/input-number"

export function PriceField() {
  const [value, setValue] = React.useState(12.5)

  return (
    <InputNumber
      value={value}
      step={0.25}
      onChange={(event) => {
        console.log("visible text", event.target.value)
      }}
      onValueChange={(next) => {
        if (next !== null) setValue(next)
      }}
    />
  )
}

Examples

Default

Visible text (onChange):

Committed value (onValueChange):

Size variants

Use size so the field aligns with InputGroup and Input dimensions. The inner InputGroupInput inherits the group size when you omit its own size.

xs

sm

default

lg