Validity

Password Input with Strength Indicator and Animation.

Strong password. Must contain:

  • At least 8 characters - Requirement met
  • At least 1 number - Requirement met
  • At least 1 lowercase letter - Requirement met
  • At least 1 uppercase letter - Requirement met

Installation

Installing dependencies

npm install zod lucide-react

Run the following command

It will create a new file validity.tsx inside the components/inputs/validity.tsx directory.

mkdir -p components/inputs && touch components/inputs/validity.tsx

Open the newly created file and paste the following code:

"use client";
 
import { Input } from "@/components/ui/input";
import { Check, Eye, EyeOff, X } from "lucide-react";
import { useEffect, useState } from "react";
import { z } from "zod";
import { motion } from "framer-motion";
 
// Define a Zod schema for password validation
const passwordSchema = z.string().superRefine((val, ctx) => {
  if (val.length < 8) {
    ctx.addIssue({
      code: z.ZodIssueCode.too_small,
      message: "At least 8 characters",
      minimum: 8,
      inclusive: true,
      type: "string",
    });
  }
  if (!/[0-9]/.test(val)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "At least 1 number",
    });
  }
  if (!/[a-z]/.test(val)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "At least 1 lowercase letter",
    });
  }
  if (!/[A-Z]/.test(val)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "At least 1 uppercase letter",
    });
  }
});
 
const InputVerification = () => {
  const [password, setPassword] = useState("");
  const [isVisible, setIsVisible] = useState<boolean>(false);
  const [errors, setErrors] = useState<string[]>([]);
 
  useEffect(() => {
    validatePassword(password);
  }, []);
 
  const toggleVisibility = () => setIsVisible((prevState) => !prevState);
 
  const validatePassword = (pass: string) => {
    const validationResult = passwordSchema.safeParse(pass);
 
    if (!validationResult.success) {
      const errorMessages = validationResult.error.issues.map(
        (issue) => issue.message
      );
      setErrors(errorMessages);
    } else {
      setErrors([]); // No errors if validation passed
    }
  };
 
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const pass = e.target.value;
    setPassword(pass);
    validatePassword(pass);
  };
 
  const strengthScore = 4 - errors.length;
 
  const getStrengthColor = (score: number) => {
    if (score === 0) return "#e0e0e0"; // Gray for empty or invalid
    if (score <= 1) return "#f87171"; // Red for weak
    if (score <= 2) return "#fb923c"; // Orange for medium
    if (score === 3) return "#facc15"; // Yellow for medium-strong
    return "#4ade80"; // Green for strong
  };
 
  const getStrengthText = (score: number) => {
    if (score === 0) return "Enter a password";
    if (score <= 2) return "Weak password";
    if (score === 3) return "Medium password";
    return "Strong password";
  };
 
  return (
    <motion.div
      className="size-full center flex-col"
      transition={{ duration: 0.5 }}
    >
      <div className="max-w-lg w-full">
        <div className="relative">
          <Input
            id="input-51"
            className="pe-9"
            placeholder="Password"
            type={isVisible ? "text" : "password"}
            value={password}
            onChange={handleChange}
            aria-invalid={strengthScore < 4}
            aria-describedby="password-strength"
          />
          <button
            className="absolute inset-y-px end-px flex h-full w-9 items-center justify-center rounded-e-lg text-muted-foreground/80 transition-shadow hover:text-foreground focus-visible:border focus-visible:border-ring focus-visible:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
            type="button"
            onClick={toggleVisibility}
            aria-label={isVisible ? "Hide password" : "Show password"}
            aria-pressed={isVisible}
            aria-controls="password"
          >
            {isVisible ? (
              <EyeOff size={16} strokeWidth={2} aria-hidden="true" />
            ) : (
              <Eye size={16} strokeWidth={2} aria-hidden="true" />
            )}
          </button>
        </div>
 
        {/* Password strength indicator */}
        <div
          className="mb-4 mt-3 h-1 w-full overflow-hidden rounded-full bg-border"
          role="progressbar"
          aria-valuenow={strengthScore}
          aria-valuemin={0}
          aria-valuemax={4}
          aria-label="Password strength"
        >
          <div
            className={`h-full transition-all duration-500 ease-out`}
            style={{
              backgroundColor: getStrengthColor(strengthScore),
              width: `${(strengthScore / 4) * 100}%`,
            }}
          ></div>
        </div>
 
        {/* Password strength description */}
        <p
          id="password-strength"
          className="mb-2 text-sm font-medium text-foreground"
        >
          {getStrengthText(strengthScore)}. Must contain:
        </p>
 
        {/* Password requirements list */}
        <ul className="space-y-1.5" aria-label="Password requirements">
          {[
            "At least 8 characters",
            "At least 1 number",
            "At least 1 lowercase letter",
            "At least 1 uppercase letter",
          ].map((reqText, index) => (
            <li key={index} className="flex items-center space-x-2">
              {errors.includes(reqText) ? (
                <X
                  size={16}
                  className="text-muted-foreground/80"
                  aria-hidden="true"
                />
              ) : (
                <Check
                  size={16}
                  className="text-emerald-500"
                  aria-hidden="true"
                />
              )}
              <span
                className={`text-xs ${
                  errors.includes(reqText)
                    ? "text-muted-foreground"
                    : "text-emerald-600"
                }`}
              >
                {reqText}
                <span className="sr-only">
                  {errors.includes(reqText)
                    ? " - Requirement not met"
                    : " - Requirement met"}
                </span>
              </span>
            </li>
          ))}
        </ul>
      </div>
    </motion.div>
  );
};
 
export default InputVerification;

Credits