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