Number Input
Numeric input with increment and decrement buttons.
Installation#
npx @aniui/cli add number-input5
Web preview — components render natively on iOS & Android
import { NumberInput } from "@/components/ui/number-input";
export function MyScreen() {
const [quantity, setQuantity] = useState(1);
return (
<NumberInput
value={quantity}
onValueChange={setQuantity}
min={1}
max={99}
step={1}
/>
);
}Usage#
app/index.tsx
import { NumberInput } from "@/components/ui/number-input";
export function MyScreen() {
const [quantity, setQuantity] = useState(1);
return (
<NumberInput
value={quantity}
onValueChange={setQuantity}
min={1}
max={99}
step={1}
/>
);
}Props#
PropTypeDefault
variant"default" | "ghost""default"size"sm" | "md" | "lg""md"valuenumber-onValueChange(value: number) => void-minnumber0maxnumber999999stepnumber1classNamestring-Also accepts all TextInput props except value and onChangeText.
Accessibility#
- Increment/decrement buttons with
accessibilityValuefor current value. - Min/max boundaries are enforced and announced to assistive technology.
Source#
components/ui/number-input.tsx
import React, { useState, useCallback } from "react";
import { View, TextInput, Pressable, Text } from "react-native";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const numberVariants = cva("flex-row items-center rounded-md border", {
variants: {
variant: {
default: "border-input bg-background",
ghost: "border-transparent bg-transparent",
},
size: {
sm: "min-h-9 px-2",
md: "min-h-12 px-3",
lg: "min-h-14 px-4",
},
},
defaultVariants: { variant: "default", size: "md" },
});
export interface NumberInputProps
extends Omit<React.ComponentPropsWithoutRef<typeof TextInput>, "value" | "onChangeText">,
VariantProps<typeof numberVariants> {
className?: string;
value?: number;
onValueChange?: (value: number) => void;
min?: number;
max?: number;
step?: number;
}
export function NumberInput({
variant,
size,
className,
value: controlledValue,
onValueChange,
min = 0,
max = 999999,
step = 1,
...props
}: NumberInputProps) {
const [internal, setInternal] = useState(controlledValue ?? min);
const value = controlledValue ?? internal;
const update = useCallback(
(next: number) => {
const clamped = Math.min(max, Math.max(min, next));
setInternal(clamped);
onValueChange?.(clamped);
},
[min, max, onValueChange]
);
return (
<View className={cn(numberVariants({ variant, size }), className)}>
<Pressable
onPress={() => update(value - step)}
disabled={value <= min}
accessible={true}
accessibilityRole="button"
accessibilityLabel="Decrease"
className="min-h-10 min-w-10 items-center justify-center"
>
<Text className={cn("text-lg font-bold", value <= min ? "text-muted" : "text-foreground")}>−</Text>
</Pressable>
<TextInput
className="flex-1 text-center text-foreground text-base p-0"
keyboardType="number-pad"
value={String(value)}
onChangeText={(t) => update(Number(t) || min)}
accessibilityLabel="Number value"
{...props}
/>
<Pressable
onPress={() => update(value + step)}
disabled={value >= max}
accessible={true}
accessibilityRole="button"
accessibilityLabel="Increase"
className="min-h-10 min-w-10 items-center justify-center"
>
<Text className={cn("text-lg font-bold", value >= max ? "text-muted" : "text-foreground")}>+</Text>
</Pressable>
</View>
);
}