AniUI

Number Input

Numeric input with increment and decrement buttons.

Installation#

npx @aniui/cli add number-input
5
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"
value
number
-
onValueChange
(value: number) => void
-
min
number
0
max
number
999999
step
number
1
className
string
-

Also accepts all TextInput props except value and onChangeText.

Accessibility#

  • Increment/decrement buttons with accessibilityValue for 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>
  );
}