AniUI

Input

Text input with variants and states.

Web preview — components render natively on iOS & Android
import { Input } from "@/components/ui/input";

export function MyScreen() {
  return (
    <Input placeholder="Enter your email..." />
  );
}

Installation#

npx @aniui/cli add input

Usage#

app/index.tsx
import { Input } from "@/components/ui/input";

export function MyScreen() {
  return (
    <Input placeholder="Enter your email..." />
  );
}

Password Toggle#

Show/hide password with a trailing eye icon.

Web preview — components render natively on iOS & Android
import { Input } from "@/components/ui/input";
import { Pressable } from "react-native";
import { Eye, EyeOff } from "lucide-react-native";

const [visible, setVisible] = useState(false);

<Input
  secureTextEntry={!visible}
  trailingIcon={
    <Pressable onPress={() => setVisible(!visible)}>
      {visible ? <EyeOff size={18} color="#71717a" /> : <Eye size={18} color="#71717a" />}
    </Pressable>
  }
  placeholder="Password"
/>

Clear Button#

Add a pressable clear icon to reset the input value.

Web preview — components render natively on iOS & Android
import { Input } from "@/components/ui/input";
import { Pressable } from "react-native";
import { XCircle } from "lucide-react-native";

<Input
  trailingIcon={
    <Pressable onPress={() => setValue("")}>
      <XCircle size={18} color="#71717a" />
    </Pressable>
  }
  placeholder="Type something..."
  value={value}
/>

Leading Icon#

Add an icon before the input text.

Web preview — components render natively on iOS & Android
import { Input } from "@/components/ui/input";
import { Search } from "lucide-react-native";

<Input
  leadingIcon={<Search size={18} color="#71717a" />}
  placeholder="Search..."
/>

Variants#

Web preview — components render natively on iOS & Android
<Input variant="default" placeholder="Default input" />
<Input variant="ghost" placeholder="Ghost input" />

Sizes#

Web preview — components render natively on iOS & Android
<Input size="sm" placeholder="Small" />
<Input size="md" placeholder="Medium" />
<Input size="lg" placeholder="Large" />

Refs#

The ref is forwarded to the underlying React Native TextInput, so you can call focus(), blur(), and clear() imperatively. Same applies to Textarea, PasswordInput, SearchBar, MaskedInput, PhoneInput, and NumberInput.

app/index.tsx
import { useRef } from "react";
import { TextInput } from "react-native";
import { Input } from "@/components/ui/input";

export function MyScreen() {
  const inputRef = useRef<TextInput>(null);

  return (
    <Input
      ref={inputRef}
      placeholder="Enter your email..."
      onSubmitEditing={() => inputRef.current?.blur()}
    />
  );
}

// Anywhere — e.g. after a button press:
//   inputRef.current?.focus();
//   inputRef.current?.blur();
//   inputRef.current?.clear();

Props#

PropTypeDefault
variant
"default" | "ghost"
"default"
size
"sm" | "md" | "lg"
"md"
leadingIcon
React.ReactNode
trailingIcon
React.ReactNode
className
string

Also accepts all TextInput props from React Native, and forwards ref to the underlying TextInput.

Accessibility#

  • accessibilityRole is set on the underlying TextInput.
  • Placeholder text color uses the theme variable for consistent contrast.

Source#

components/ui/input.tsx
import React from "react";
import { View, TextInput, useColorScheme } from "react-native";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

// Padding lives on the wrapping View, never on the raw TextInput — a TextInput
// doesn't honor `px-*` reliably, so keeping padding on the View gives a
// consistent inset with or without icons, under both NativeWind and Uniwind.
const inputVariants = cva(
  "flex-row items-center rounded-md border py-2",
  {
    variants: {
      variant: {
        default: "border-input bg-background",
        ghost: "border-transparent bg-transparent",
      },
      size: {
        sm: "min-h-9 px-3",
        md: "min-h-12 px-4",
        lg: "min-h-14 px-5",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "md",
    },
  }
);

const fontSizes = { sm: 14, md: 16, lg: 18 } as const;

export interface InputProps
  extends React.ComponentPropsWithoutRef<typeof TextInput>,
    VariantProps<typeof inputVariants> {
  className?: string;
  leadingIcon?: React.ReactNode;
  trailingIcon?: React.ReactNode;
}

export const Input = React.forwardRef<
  React.ElementRef<typeof TextInput>,
  InputProps
>(function Input(
  { variant, size, className, leadingIcon, trailingIcon, style, ...props },
  ref
) {
  const dark = useColorScheme() === "dark";
  const caret = dark ? "#fafafa" : "#18181b";
  const resolvedSize = size ?? "md";

  return (
    <View className={cn(inputVariants({ variant, size }), className)}>
      {leadingIcon && <View className="me-2">{leadingIcon}</View>}
      <TextInput
        ref={ref}
        // font-size is set inline (no lineHeight) so the cursor stays centered
        // on iOS while the size still matches the variant on every platform.
        // self-stretch makes the TextInput fill the row height so taps on the
        // wrapper's vertical padding still focus the field.
        className="flex-1 self-stretch p-0 text-foreground placeholder:text-muted-foreground"
        style={[{ fontSize: fontSizes[resolvedSize] }, style]}
        textAlignVertical="center"
        placeholderTextColor={dark ? "#a1a1aa" : "#71717a"}
        keyboardAppearance={dark ? "dark" : "light"}
        selectionColor={caret}
        cursorColor={caret}
        {...props}
      />
      {trailingIcon && <View className="ms-2">{trailingIcon}</View>}
    </View>
  );
});