Shadcn Number Input for React and Tailwind
Numeric input for entering values.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/number-input.jsonpnpm dlx shadcn@latest add https://kit.dev/r/number-input.jsonnpx shadcn@latest add https://kit.dev/r/number-input.jsonyarn shadcn@latest add https://kit.dev/r/number-input.json<Step>This component depends on Button, Field, and Input. Install them first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react lucide-reactpnpm add @ark-ui/react lucide-reactnpm install @ark-ui/react lucide-reactyarn add @ark-ui/react lucide-reactCopy and paste the following code into your project.
"use client";
import {
NumberInput as ArkNumberInput,
useNumberInputContext,
} from "@ark-ui/react/number-input";
import { MinusIcon, PlusIcon } from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { FieldLabel } from "@/components/ui/field";
import { Input, type InputProps } from "@/components/ui/input";
export const useNumberInput = useNumberInputContext;
interface NumberInputProps
extends React.ComponentProps<typeof ArkNumberInput.Root>,
Pick<InputProps, "size"> {}
export const NumberInput = (props: NumberInputProps) => {
const { size = "md", className, ...rest } = props;
return (
<ArkNumberInput.Root
className={cn(
"group/number-field",
"flex w-full flex-col items-start gap-2",
"has-data-[slot=number-field-increment]:has-data-[slot=number-field-decrement]:**:data-[slot=number-field-input]:text-center",
className
)}
data-size={size}
data-slot="number-field"
{...rest}
/>
);
};
export const NumberInputGroup = (
props: React.ComponentProps<typeof ArkNumberInput.Control>
) => {
const { className, ...rest } = props;
return (
<ArkNumberInput.Control
className={cn(
"relative",
"w-full",
"flex justify-between",
"bg-transparent dark:bg-input/30",
"text-base",
"rounded-lg border border-input shadow-xs/5 ring-ring/32",
"transition-shadow",
"focus-within:border-primary focus-within:ring-[3px] focus-within:ring-ring/32",
"data-disabled:pointer-events-none data-disabled:opacity-64",
"aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/24",
"dark:aria-invalid:border-destructive-foreground dark:aria-invalid:text-destructive-foreground dark:aria-invalid:ring-destructive-foreground/20",
"motion-reduce:transition-none!",
className
)}
data-slot="number-field-group"
{...rest}
/>
);
};
export const NumberInputDecrement = (
props: React.ComponentProps<typeof ArkNumberInput.DecrementTrigger>
) => {
const { className, ...rest } = props;
return (
<ArkNumberInput.DecrementTrigger
asChild
className={cn(
"relative",
"h-8 in-data-[size=lg]:h-9 in-data-[size=sm]:h-7",
"flex shrink-0",
"text-foreground",
"rounded-none rounded-s-[calc(var(--radius-lg)+1px)]",
"cursor-pointer",
"pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11",
className
)}
data-slot="number-field-decrement"
{...rest}
>
<Button aria-label="Decrement" variant="ghost">
<MinusIcon aria-hidden />
</Button>
</ArkNumberInput.DecrementTrigger>
);
};
export const NumberInputIncrement = (
props: React.ComponentProps<typeof ArkNumberInput.IncrementTrigger>
) => {
const { className, ...rest } = props;
return (
<ArkNumberInput.IncrementTrigger
asChild
className={cn(
"relative",
"h-8 in-data-[size=lg]:h-9 in-data-[size=sm]:h-7",
"flex shrink-0",
"text-foreground",
"rounded-none rounded-e-[calc(var(--radius-lg)+1px)]",
"cursor-pointer",
"pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11",
className
)}
data-slot="number-field-increment"
{...rest}
>
<Button aria-label="Increment" variant="ghost">
<PlusIcon aria-hidden />
</Button>
</ArkNumberInput.IncrementTrigger>
);
};
export const NumberInputInput = (props: React.ComponentProps<typeof Input>) => {
const { size, className, ...rest } = props;
return (
<ArkNumberInput.Input asChild data-slot="number-field-input" {...rest}>
<Input
className={cn(
"grow",
"h-8 in-data-[size=lg]:h-9 in-data-[size=sm]:h-7",
"tabular-nums",
"border-0 shadow-none ring-0",
"focus-visible:ring-0 aria-invalid:ring-0 data-invalid:ring-0",
"dark:bg-transparent",
className
)}
/>
</ArkNumberInput.Input>
);
};
export const NumberInputScrubber = (
props: React.ComponentProps<typeof ArkNumberInput.Scrubber>
) => {
const { className, children, ...rest } = props;
return (
<ArkNumberInput.Scrubber
asChild
className={cn("flex cursor-ew-resize", className)}
data-slot="number-field-scrubber"
{...rest}
>
<ArkNumberInput.Label asChild>
<FieldLabel>{children}</FieldLabel>
</ArkNumberInput.Label>
</ArkNumberInput.Scrubber>
);
};Update the import paths to match your project setup.
Anatomy
NumberInput
└── NumberInputGroup
├── NumberInputDecrement
├── NumberInputInput
├── NumberInputIncrement
└── NumberInputScrubberUsage
import {
NumberInput,
NumberInputGroup,
NumberInputInput,
NumberInputIncrement,
NumberInputDecrement
} from "@/comopnents/ui/number-input";<NumberInput>
<NumberInputGroup>
<NumberInputInput />
<NumberInputIncrement />
<NumberInputDecrement />
</NumberInputGroup>
</NumberInput>Controlled
Use value and onValueChange to control the number input. Keep the value as a string when using formatOptions to preserve locale formatting.
States
Disabled
Use the disabled prop to disable the number input.
Invalid
Use the invalid prop to mark the number input as invalid.
Sizes
Use the size prop to change the input height.
Small
Medium
Large
Examples
Field Only
With Field
Use Field components for a labeled number input with helper text.
With Input Group
Use InputGroup, InputGroupAddon, and InputGroupText to add prefix or suffix.
With Scrub
Use the NumberInputScrubber to drag the label to change the value.
With Range
Pass min and max to restrict the value to the specified range.
Use clampValueOnBlur={false} to allow typing outside range until blur.
With Formatted Value
Use formatOptions with Intl.NumberFormatOptions to format the displayed value.
With Step
Use the step prop to set the increment/decrement amount.
Mouse Wheel
Use the allowMouseWheel prop to increment or decrement the value with the mouse wheel.
API Reference
NumberInput
Root component. Manages numeric value and stepper controls.
| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | "md" |
scrubber | boolean | false |
defaultValue | string | - |
value | string | - |
min | number | Number.MIN_SAFE_INTEGER |
max | number | Number.MAX_SAFE_INTEGER |
step | number | 1 |
formatOptions | Intl.NumberFormatOptions | - |
disabled | boolean | - |
invalid | boolean | - |
focusInputOnChange | boolean | true |
clampValueOnBlur | boolean | true |
allowMouseWheel | boolean | false |
onValueChange | (details: ValueChangeDetails) => void | - |
className | string | - |
asChild | boolean | false |
NumberInputGroup
Wraps the input and increment/decrement buttons.
| Prop | Type | Default |
|---|---|---|
className | string | - |
asChild | boolean | false |
NumberInputDecrement
Decreases the value by the step amount.
| Prop | Type | Default |
|---|---|---|
className | string | - |
asChild | boolean | false |
NumberInputIncrement
Increases the value by the step amount.
| Prop | Type | Default |
|---|---|---|
className | string | - |
asChild | boolean | false |
NumberInputInput
Numeric input. Accepts number or direct typing.
| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | "md" |
className | string | - |
asChild | boolean | false |
NumberInputScrubber
Drag area for scrubbing the value.
| Prop | Type | Default |
|---|---|---|
className | string | - |
asChild | boolean | false |
For a complete list of props, see the Ark UI documentation.