Password strength

Password-strength animation when the user enters their password for confirmation of strength.

Installation

Run the following command

It will create a new file password-strength.tsx inside the components/inputs/password-strength.tsx directory.

mkdir -p components/inputs && touch components/inputs/password-strength.tsx

Paste the code

Open the newly created file and paste the following code:

"use client";
 
import {
  motion,
  useTransform,
  useMotionValue,
  useAnimationControls,
} from "framer-motion";
import React, { useState, useEffect } from "react";
import { LucideEye, LucideEyeOff } from "lucide-react";
import { cn } from "@/lib/utils";
 
const PasswordStrength = () => {
  const [value, setValue] = useState("");
  const passwordLength = useMotionValue(value.length);
  const [showPassword, setShowPassword] = useState(false); // To toggle password visibility
  const background = useTransform(
    passwordLength,
    [0, 3, 6, 8],
    ["#ffcccb", "#ffa07a", "#90ee90", "#32cd32"]
  );
  const scaleControls = useAnimationControls();
 
  const [percentage, setPercentage] = useState(0);
 
  const words = [
    "",
    "Very weak",
    "Weak",
    "Fair",
    "Moderate",
    "Good",
    "Strong",
    "Very Strong",
    "Excellent",
  ];
 
  useEffect(() => {
    const val = (value.length / 8) * 100;
    setPercentage(val > 100 ? 100 : val);
    passwordLength.set(value.length);
 
    //for the scaling down and up of the input
    if (value.length === 8) {
      scaleControls.start({
        scale: [1, 0.9, 1.1, 1],
        transition: { duration: 0.5 },
      });
    }
  }, [value, passwordLength, scaleControls]);
 
  return (
    <div className="h-full w-full center">
      <motion.div
        layout
        transition={{ duration: 0.1, ease: "easeInOut" }}
        className="max-w-lg mx-auto w-full center flex-col gap-4"
      >
        <motion.div
          animate={scaleControls}
          className="w-full h-20 rounded-lg bg-muted p-2 relative z-0 overflow-hidden"
        >
          <div className="h-full  m-auto w-full bg-primary rounded-lg overflow-hidden">
            <input
              onChange={(e) => setValue(e.target.value)}
              placeholder="Enter your password"
              type={showPassword ? "text" : "password"}
              className="h-full w-full outline-none border-none px-5 pr-12 z-10 text-muted-foreground"
            />{" "}
            <button
              className="my-auto absolute right-5 top-0 bottom-0 size-10 center border rounded"
              onClick={() => setShowPassword((prev) => !prev)}
            >
              {showPassword ? (
                <LucideEye
                  onClick={() => setShowPassword(false)} // Hide password
                  className="size-4 cursor-pointer text-muted-foreground pointer-events-none"
                />
              ) : (
                <LucideEyeOff
                  onClick={() => setShowPassword(true)} // Show password
                  className="size-4 cursor-pointer text-muted-foreground pointer-events-none"
                />
              )}
            </button>
            <motion.div
              style={{
                background,
              }}
              animate={{ width: `${percentage}%` }}
              className={cn(`absolute top-0 left-0 bg-primary h-full -z-10`)}
            />
          </div>
        </motion.div>
        <p className="text-muted-foreground">
          {words[value.length > 8 ? words.length - 1 : value.length]}
        </p>
      </motion.div>
    </div>
  );
};
 
export default PasswordStrength;

With Zod

npm install zod
"use client";
 
import { motion, useAnimationControls } from "framer-motion";
import React, { useState, useEffect } from "react";
import { LucideEye, LucideEyeOff } from "lucide-react";
import { cn } from "@/lib/utils";
import { z } from "zod";
 
// Zod schema for password validation, now including uppercase letter check
const passwordSchema = z
  .string()
  .min(8, "Password must be at least 8 characters")
  .regex(/[a-zA-Z]/, "Password must contain at least one letter")
  .regex(/[A-Z]/, "Password must contain at least one uppercase letter")
  .regex(/\d/, "Password must contain at least one number")
  .regex(/[\W_]/, "Password must contain at least one special character");
 
const PasswordStrength = () => {
  const [value, setValue] = useState("");
  const [validationError, setValidationError] = useState<string | null>(null);
  const [validationProgress, setValidationProgress] = useState(0); // Track validation progress
  const [hasScaled, setHasScaled] = useState(false); // To track if scaling has already occurred
  const [showPassword, setShowPassword] = useState(false); // To toggle password visibility
  const scaleControls = useAnimationControls();
 
  const getBackgroundColor = (progress: number) => {
    // Background colors change as validation progresses
    if (progress === 0) return "#ffcccb"; // Red for invalid
    if (progress < 100) return "#ffa07a"; // Orange for partial validation
    return "#32cd32"; // Green for full validation
  };
 
  useEffect(() => {
    const checks = {
      length: false,
      letter: false,
      uppercase: false,
      number: false,
      special: false,
    };
 
    // Check each zod validation condition independently
    if (value.length >= 8) checks.length = true;
    if (/[a-zA-Z]/.test(value)) checks.letter = true;
    if (/[A-Z]/.test(value)) checks.uppercase = true; // Uppercase check
    if (/\d/.test(value)) checks.number = true;
    if (/[\W_]/.test(value)) checks.special = true;
 
    // Calculate validation progress based on how many checks pass
    const progress =
      (Object.values(checks).filter((pass) => pass).length / 5) * 100;
    setValidationProgress(progress);
 
    // Set error if not fully valid
    try {
      passwordSchema.parse(value);
      setValidationError(null); // Clear error if valid
    } catch (err: any) {
      setValidationError(err.errors[0].message); // Set error message if invalid
    }
 
    // Trigger scaling only once when validation reaches 100%
    if (progress === 100 && !hasScaled) {
      scaleControls.start({
        scale: [1, 0.9, 1.1, 1],
        transition: { duration: 0.5 },
      });
      setHasScaled(true); // Mark as scaled to prevent repeated scaling
    }
  }, [value, scaleControls, hasScaled]);
 
  return (
    <div className="h-full w-full center">
      <motion.div
        layout
        transition={{ duration: 0.1, ease: "easeInOut" }}
        className="max-w-lg mx-auto w-full center flex-col gap-4"
      >
        <motion.div
          animate={scaleControls}
          className="w-full h-20 rounded-lg bg-muted p-2 relative z-0 overflow-hidden"
        >
          <div className="h-full m-auto w-full bg-primary rounded-lg overflow-hidden">
            <input
              onChange={(e) => {
                setValue(e.target.value);
                if (validationProgress < 100) setHasScaled(false); // Reset scaling when progress drops below 100%
              }}
              placeholder="Enter your password"
              type={showPassword ? "text" : "password"}
              className="h-full w-full outline-none border-none px-5 pr-12 z-10 text-muted-foreground"
            />
            <button
              className="my-auto absolute right-5 top-0 bottom-0 size-10 center border rounded"
              onClick={() => setShowPassword((prev) => !prev)}
            >
              {showPassword ? (
                <LucideEye
                  onClick={() => setShowPassword(false)} // Hide password
                  className="size-4 cursor-pointer text-muted-foreground pointer-events-none"
                />
              ) : (
                <LucideEyeOff
                  onClick={() => setShowPassword(true)} // Show password
                  className="size-4 cursor-pointer text-muted-foreground pointer-events-none"
                />
              )}
            </button>
            <motion.div
              style={{
                background: getBackgroundColor(validationProgress), // Dynamic background based on validation progress
              }}
              animate={{ width: `${validationProgress}%` }} // Width increases with validation progress
              className={cn(`absolute top-0 left-0 h-full -z-10`)}
            />
          </div>
        </motion.div>
        {validationError && value.length !== 0 && (
          <p className="text-red-500 text-sm mt-2">{validationError}</p>
        )}
      </motion.div>
    </div>
  );
};
 
export default PasswordStrength;

Credits

Built by Bossadi Zenith

inspired by Lndev