Bloom Logo

Password Input

A specialized password input field equipped with configurable validation rules, real-time feedback tick/cross indicators, and a visual password strength meter.

importimport { PasswordInput } from "@/components/ui/passwordInput/passwordInput";
$npx @bloomui-react/cli add passwordInput
passwordInput.tsx
"use client";

import { Icon } from "@iconify/react";
import * as React from "react";
import { Input } from "@/components/ui/input/input";
import { cn } from "@/lib/utils";

export interface PasswordRule {
  id: string;
  label: string;
  validate: (value: string) => boolean;
}

export interface PasswordInputProps
  extends Omit<React.ComponentPropsWithoutRef<typeof Input>, "type"> {
  variant?:
    | "default"
    | "bordered"
    | "flat"
    | "underlined"
    | "filled"
    | "glassmorphism"
    | "gradient-border"
    | "glow";
  minLength?: number;
  requireUppercase?: boolean;
  requireLowercase?: boolean;
  requireNumber?: boolean;
  requireSymbol?: boolean;
  customRules?: PasswordRule[];
  showRequirements?: "always" | "on-focus" | "never";
  showStrengthMeter?: boolean;
  onValidityChange?: (isValid: boolean) => void;
}

const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
  (
    {
      className,
      minLength = 8,
      requireUppercase = true,
      requireLowercase = true,
      requireNumber = true,
      requireSymbol = true,
      customRules,
      showRequirements = "on-focus",
      showStrengthMeter = true,
      onValidityChange,
      value,
      defaultValue,
      onChange,
      onFocus,
      onBlur,
      ...props
    },
    ref,
  ) => {
    const [internalValue, setInternalValue] = React.useState(
      (value !== undefined
        ? value
        : defaultValue !== undefined
          ? defaultValue
          : ""
      )?.toString() || "",
    );
    const [isFocused, setIsFocused] = React.useState(false);

    const isControlled = value !== undefined;
    const currentValue = isControlled ? value.toString() : internalValue;

    const rules = React.useMemo(() => {
      const activeRules: PasswordRule[] = [];

      if (minLength > 0) {
        activeRules.push({
          id: "min-length",
          label: `At least ${minLength} characters`,
          validate: (val) => val.length >= minLength,
        });
      }
      if (requireUppercase) {
        activeRules.push({
          id: "uppercase",
          label: "At least one uppercase letter",
          validate: (val) => /[A-Z]/.test(val),
        });
      }
      if (requireLowercase) {
        activeRules.push({
          id: "lowercase",
          label: "At least one lowercase letter",
          validate: (val) => /[a-z]/.test(val),
        });
      }
      if (requireNumber) {
        activeRules.push({
          id: "number",
          label: "At least one number",
          validate: (val) => /[0-9]/.test(val),
        });
      }
      if (requireSymbol) {
        activeRules.push({
          id: "symbol",
          label: "At least one special character",
          validate: (val) => /[^A-Za-z0-9]/.test(val),
        });
      }

      if (customRules) {
        activeRules.push(...customRules);
      }

      return activeRules;
    }, [
      minLength,
      requireUppercase,
      requireLowercase,
      requireNumber,
      requireSymbol,
      customRules,
    ]);

    const ruleStatuses = React.useMemo(() => {
      return rules.map((rule) => ({
        ...rule,
        isValid: rule.validate(currentValue),
      }));
    }, [rules, currentValue]);

    const allValid = React.useMemo(() => {
      return ruleStatuses.every((r) => r.isValid);
    }, [ruleStatuses]);

    React.useEffect(() => {
      onValidityChange?.(allValid);
    }, [allValid, onValidityChange]);

    const strengthPercentage = React.useMemo(() => {
      if (rules.length === 0) return 0;
      const validCount = ruleStatuses.filter((r) => r.isValid).length;
      return Math.round((validCount / rules.length) * 100);
    }, [ruleStatuses, rules]);

    const strengthColor = React.useMemo(() => {
      if (strengthPercentage < 40) return "bg-rose-500";
      if (strengthPercentage < 80) return "bg-amber-500";
      return "bg-emerald-500";
    }, [strengthPercentage]);

    const strengthText = React.useMemo(() => {
      if (strengthPercentage === 0) return "Empty";
      if (strengthPercentage < 40) return "Weak";
      if (strengthPercentage < 80) return "Medium";
      return "Strong";
    }, [strengthPercentage]);

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      if (!isControlled) {
        setInternalValue(e.target.value);
      }
      onChange?.(e);
    };

    const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
      setIsFocused(true);
      onFocus?.(e);
    };

    const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
      setIsFocused(false);
      onBlur?.(e);
    };

    const shouldShowRequirements =
      showRequirements === "always" ||
      (showRequirements === "on-focus" &&
        (isFocused || currentValue.length > 0));

    return (
      <div className="w-full flex flex-col gap-2">
        <Input
          ref={ref}
          type="password"
          isPasswordToggle
          value={currentValue}
          onChange={handleChange}
          onFocus={handleFocus}
          onBlur={handleBlur}
          autoComplete="off"
          {...props}
        />

        {shouldShowRequirements && rules.length > 0 && (
          <div className="w-full p-4 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 transition-all duration-300 animate-in fade-in slide-in-from-top-2">
            {showStrengthMeter && (
              <div className="mb-4">
                <div className="flex items-center justify-between text-xs mb-1.5">
                  <span className="text-zinc-500 dark:text-zinc-400 font-medium">
                    Password Strength
                  </span>
                  <span
                    className={cn(
                      "font-semibold",
                      strengthPercentage < 40
                        ? "text-rose-500"
                        : strengthPercentage < 80
                          ? "text-amber-500"
                          : "text-emerald-500",
                    )}
                  >
                    {strengthText}
                  </span>
                </div>
                <div className="h-1.5 w-full bg-zinc-100 dark:bg-zinc-800 rounded-full overflow-hidden">
                  <div
                    className={cn(
                      "h-full transition-all duration-300",
                      strengthColor,
                    )}
                    style={{ width: `${strengthPercentage}%` }}
                  />
                </div>
              </div>
            )}

            <ul className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs">
              {ruleStatuses.map((rule) => (
                <li
                  key={rule.id}
                  className={cn(
                    "flex items-center gap-2 transition-colors duration-200",
                    rule.isValid
                      ? "text-emerald-600 dark:text-emerald-400"
                      : currentValue.length > 0
                        ? "text-rose-600 dark:text-rose-400"
                        : "text-zinc-500 dark:text-zinc-400",
                  )}
                >
                  <Icon
                    icon={
                      rule.isValid
                        ? "hugeicons:tick-02"
                        : currentValue.length > 0
                          ? "hugeicons:cancel-01"
                          : "hugeicons:circle"
                    }
                    className="size-4 shrink-0 transition-transform duration-300"
                  />
                  <span className="font-medium">{rule.label}</span>
                </li>
              ))}
            </ul>
          </div>
        )}
      </div>
    );
  },
);

PasswordInput.displayName = "PasswordInput";

export { PasswordInput };

Default

A basic password input with default requirements (8 characters min, uppercase, lowercase, number, symbol).

Form valid: No

Variants

Defines the visual appearance of the password input using the variant prop.

variant: default | bordered | flat | filled | glow | glassmorphism | gradient-border | underlined

Custom Validation Rules

Define custom password criteria by passing an array of rules with validation functions.

spaces: anystarts: anywith: anya: anyletter: anycustomRules: anyminLength: anyrequireSymbol: anyfalse: any

Show Requirements Mode

Control when the password requirement panel is visible: always, on-focus (default), or never.

showRequirements: anyalways: anynever: any
Password StrengthEmpty
  • At least 8 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one number
  • At least one special character

Hide Strength Meter

Disable the password strength progress bar indicator.

showStrengthMeter: anyfalse: any

Props — PasswordInput

Supported properties for the PasswordInput component.

PropTypeDefaultDescription
minLengthnumber8Minimum length requirement. Set to 0 to disable.
requireUppercasebooleantrueRequires at least one uppercase letter.
requireLowercasebooleantrueRequires at least one lowercase letter.
requireNumberbooleantrueRequires at least one number digit.
requireSymbolbooleantrueRequires at least one special character/symbol.
customRulesPasswordRule[][]Array of custom validation rules.
showRequirements"always" | "on-focus" | "never""on-focus"Visibility of the requirements validation card helper.
showStrengthMeterbooleantrueToggles visibility of the password strength progress bar.
onValidityChange(isValid: boolean) => voidCallback function triggered when the overall password validation status changes.