The NumberInput component provides multiple ways to input numeric values: direct text entry, increment/decrement buttons, and interactive scrubbing.
Installation
npx shadcn@latest add @eo-n/number-input
Install dependencies
Install the required packages:npm install @base-ui/react lucide-react
Copy component code
Copy and paste the number-input component code into your project at components/ui/number-input.tsx.
Update imports
Update the import paths to match your project setup.
import {
NumberInput,
NumberInputDecrement,
NumberInputField,
NumberInputGroup,
NumberInputIncrement,
} from "@/components/ui/number-input";
export default function Example() {
return (
<NumberInput>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField />
<NumberInputIncrement />
</NumberInputGroup>
</NumberInput>
);
}
Examples
Default
<NumberInput defaultValue={0}>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField />
<NumberInputIncrement />
</NumberInputGroup>
</NumberInput>
With Label and Scrub Area
import {
NumberInputScrubArea,
NumberInputScrubAreaCursor,
} from "@/components/ui/number-input";
import { Label } from "@/components/ui/label";
<NumberInput defaultValue={25}>
<NumberInputScrubArea>
<Label>Age</Label>
</NumberInputScrubArea>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField />
<NumberInputIncrement />
</NumberInputGroup>
</NumberInput>
Bounded Number Input (Min/Max)
<NumberInput defaultValue={50} min={0} max={100}>
<NumberInputScrubArea>
<Label>Volume (0-100)</Label>
</NumberInputScrubArea>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField />
<NumberInputIncrement />
</NumberInputGroup>
</NumberInput>
With Custom Step
<NumberInput defaultValue={0} step={5}>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField />
<NumberInputIncrement />
</NumberInputGroup>
</NumberInput>
Decimal Values
<NumberInput defaultValue={0.0} step={0.1} min={0} max={1}>
<NumberInputScrubArea>
<Label>Opacity (0.0 - 1.0)</Label>
</NumberInputScrubArea>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField />
<NumberInputIncrement />
</NumberInputGroup>
</NumberInput>
Disabled
<NumberInput defaultValue={42} disabled>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField />
<NumberInputIncrement />
</NumberInputGroup>
</NumberInput>
Scrub Direction
<NumberInput defaultValue={0}>
<NumberInputScrubArea direction="horizontal">
<Label>Horizontal Scrub</Label>
</NumberInputScrubArea>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField />
<NumberInputIncrement />
</NumberInputGroup>
</NumberInput>
Controlled
import { useState } from "react";
function ControlledNumberInput() {
const [value, setValue] = useState(0);
return (
<div className="space-y-4">
<NumberInput value={value} onValueChange={setValue}>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField />
<NumberInputIncrement />
</NumberInputGroup>
</NumberInput>
<p className="text-sm">Value: {value}</p>
</div>
);
}
In a Form
function FormExample() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
console.log(formData.get('quantity'));
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="quantity">Quantity</Label>
<NumberInput name="quantity" defaultValue={1} min={1}>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField id="quantity" />
<NumberInputIncrement />
</NumberInputGroup>
</NumberInput>
</div>
<button type="submit">Submit</button>
</form>
);
}
API Reference
NumberInput
Extends all props from @base-ui/react NumberField.Root component.
The controlled value of the number input.
The default value when uncontrolled.
Callback fired when the value changes.(value: number | null) => void
The minimum allowed value.
The maximum allowed value.
The increment/decrement step value.
Whether the number input is disabled.
Whether the number input is required in a form.
The name attribute for form submission.
Allow changing value with mouse wheel.
Additional CSS classes to apply.
NumberInputScrubArea
Interactive area for scrubbing (dragging) to change values.
direction
string
default:"horizontal"
The scrub direction.Options: horizontal | vertical
Pixels to drag per step increment.
Additional CSS classes to apply.
NumberInputGroup
Container for the decrement button, field, and increment button.
Additional CSS classes to apply.
NumberInputDecrement
Button to decrease the value.
Additional CSS classes to apply.
NumberInputField
The input field for direct text entry.
The id for associating with a label.
Placeholder text for the input.
Additional CSS classes to apply.
NumberInputIncrement
Button to increase the value.
Additional CSS classes to apply.
TypeScript
import { NumberField as NumberInputPrimitive } from "@base-ui/react";
type NumberInputProps = React.ComponentProps<typeof NumberInputPrimitive.Root>;
type NumberInputFieldProps = React.ComponentProps<typeof NumberInputPrimitive.Input>;
type NumberInputScrubAreaProps = React.ComponentProps<typeof NumberInputPrimitive.ScrubArea>;
Styling Features
- Tabular nums for aligned digits
- Focus ring on keyboard focus
- Hover states for buttons
- Disabled state styling
- Dark mode support
- Interactive scrub cursor
- Validation error states via
aria-invalid
- Shadow effects for depth
Accessibility
- Fully keyboard accessible (Arrow keys, Page Up/Down)
- Proper ARIA attributes
- Focus visible indicators
- Works with form labels
- Screen reader support
- Touch-friendly buttons
Related
- Input - For text input with number type
- Slider - For visual range selection