AniUI

Password Input

Password input with show/hide toggle and optional strength indicator.

Installation#

npx @aniui/cli add password-input
Web preview — components render natively on iOS & Android
import { PasswordInput } from "@/components/ui/password-input";

export function MyScreen() {
  return (
    <PasswordInput
      placeholder="Enter password"
      showStrength
      onChangeText={(text) => console.log(text)}
    />
  );
}

Usage#

app/index.tsx
import { PasswordInput } from "@/components/ui/password-input";

export function MyScreen() {
  return (
    <PasswordInput
      placeholder="Enter password"
      showStrength
      onChangeText={(text) => console.log(text)}
    />
  );
}

Props#

PropTypeDefault
variant
"default" | "ghost"
"default"
size
"sm" | "md" | "lg"
"md"
showStrength
boolean
false
className
string
-
onChangeText
(text: string) => void
-

Also accepts all TextInput props except secureTextEntry.

Accessibility#

  • Secure text entry with show/hide toggle button.
  • Toggle button has accessibilityLabel indicating current visibility state.

Source#

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

const passwordVariants = cva(
  "flex-row items-center rounded-md border py-2 text-foreground",
  {
    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" },
  }
);

export interface PasswordInputProps
  extends Omit<React.ComponentPropsWithoutRef<typeof TextInput>, "secureTextEntry">,
    VariantProps<typeof passwordVariants> {
  className?: string;
  showStrength?: boolean;
}

function getStrength(value: string): number {
  let score = 0;
  if (value.length >= 8) score++;
  if (/[A-Z]/.test(value)) score++;
  if (/[0-9]/.test(value)) score++;
  if (/[^A-Za-z0-9]/.test(value)) score++;
  return score;
}

const strengthColors = ["bg-destructive", "bg-orange-500", "bg-yellow-500", "bg-green-500"];
const strengthLabels = ["Weak", "Fair", "Good", "Strong"];

export function PasswordInput({
  variant,
  size,
  className,
  showStrength,
  onChangeText,
  ...props
}: PasswordInputProps) {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState("");
  const strength = getStrength(value);

  return (
    <View className="gap-2">
      <View className={cn(passwordVariants({ variant, size }), className)}>
        <TextInput
          className="flex-1 text-foreground p-0 text-base"
          placeholderTextColor="hsl(240 3.8% 46.1%)"
          secureTextEntry={!visible}
          onChangeText={(text) => { setValue(text); onChangeText?.(text); }}
          accessibilityLabel="Password"
          {...props}
        />
        <Pressable
          onPress={() => setVisible(!visible)}
          accessible={true}
          accessibilityRole="button"
          accessibilityLabel={visible ? "Hide password" : "Show password"}
          className="ml-2 min-h-8 min-w-8 items-center justify-center"
        >
          <Text className="text-muted-foreground text-sm">{visible ? "Hide" : "Show"}</Text>
        </Pressable>
      </View>
      {showStrength && value.length > 0 && (
        <View className="flex-row items-center gap-2">
          <View className="flex-1 flex-row gap-1">
            {[0, 1, 2, 3].map((i) => (
              <View
                key={i}
                className={cn("flex-1 h-1 rounded-full", i < strength ? strengthColors[strength - 1] : "bg-muted")}
              />
            ))}
          </View>
          <Text className="text-xs text-muted-foreground">{strengthLabels[strength - 1] ?? ""}</Text>
        </View>
      )}
    </View>
  );
}