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
| Key | Behavior |
|---|---|
| ↑ | 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. |
blur | Commits the same way as ↵ when focus leaves the input. |
Custom props
| Prop | Type | Description |
|---|---|---|
value | string | number | Controlled value. Strings control the visible text; numbers control the numeric state. |
onValueChange | (value: number | null) => void | Fires when the committed numeric value changes (see Overview and Key bindings). |
min | number | Optional minimum clamp. Ignored if min > max. |
max | number | Optional maximum clamp. Ignored if min > max. |
step | number | Step 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-numberThis 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