Verifier

A comprehensive guide to using the Verifier component for password validation with interactive feedback and animations.

Installation

Run the following command

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

Paste the code

Open the newly created file and paste the following code:

"use client";
 
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import { Check, X } from "lucide-react";
 
const Verifier = () => {
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");
  const [shake, setShake] = useState(false);
  const [showConfirm, setShowConfirm] = useState(false);
 
  const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPassword(e.target.value);
  };
 
  const handleConfirmPasswordChange = (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    if (
      confirmPassword.length >= password.length &&
      e.target.value.length > confirmPassword.length
    ) {
      setShake(true);
    } else {
      setConfirmPassword(e.target.value);
    }
  };
 
  useEffect(() => {
    if (shake) {
      const timer = setTimeout(() => setShake(false), 500);
      return () => clearTimeout(timer);
    }
  }, [shake]);
 
  const getLetterStatus = (letter: string, index: number) => {
    if (!confirmPassword[index]) return "";
    return confirmPassword[index] === letter
      ? "bg-green-500/30"
      : "bg-red-500/30";
  };
 
  const passwordsMatch = password === confirmPassword && password.length > 0;
 
  const bounceAnimation = {
    x: shake ? [-10, 10, -10, 10, 0] : 0,
    transition: { duration: 0.5 },
  };
 
  const matchAnimation = {
    // scale: passwordsMatch ? [1, 1.05, 1] : 1,
    transition: { duration: 0.3 },
  };
 
  const borderAnimation = {
    borderColor: passwordsMatch ? "#22c55e" : "",
    transition: { duration: 0.3 },
  };
 
  return (
    <main className="relative flex size-full w-full items-start justify-center px-4 py-10 md:items-center">
      <div className="z-10 flex w-full flex-col items-center">
        <div className="mx-auto flex h-full w-full max-w-lg flex-col items-center justify-center gap-8 p-16">
          <div className="relative flex w-full flex-col items-start justify-center gap-4">
            <div className="w-full">
              {!showConfirm ? (
                <motion.input
                  className="h-[52px] w-full rounded-xl border-2 px-3.5 py-3 tracking-[.59rem] outline-none placeholder:tracking-widest focus:border-foreground-muted"
                  type="password"
                  placeholder="Enter Password"
                  value={password}
                  onChange={handlePasswordChange}
                />
              ) : (
                <motion.div
                  className={cn(
                    "h-[52px] w-full rounded-xl border-2 px-2 py-2 bg-muted/40"
                  )}
                  animate={{
                    ...bounceAnimation,
                    ...matchAnimation,
                    ...borderAnimation,
                  }}
                >
                  <div className="relative h-full w-fit overflow-hidden rounded-lg ">
                    <div className="z-10 flex h-full items-center justify-center px-0 py-1">
                      {password.split("").map((_, index) => (
                        <div
                          key={index}
                          className="flex h-full w-4 shrink-0 items-center justify-center"
                        >
                          <span className="size-[5px] rounded-full bg-muted-foreground" />
                        </div>
                      ))}
                    </div>
 
                    <div className="absolute bottom-0 left-0 top-0 z-0 flex h-full w-full items-center justify-start">
                      {password.split("").map((letter, index) => (
                        <motion.div
                          key={index}
                          className={cn(
                            "ease absolute h-full w-4 transition-all duration-300",
                            getLetterStatus(letter, index)
                          )}
                          style={{
                            left: `${index * 16}px`,
                            scaleX: confirmPassword[index] ? 1 : 0,
                            transformOrigin: "left",
                          }}
                        />
                      ))}
                    </div>
                  </div>
                </motion.div>
              )}
            </div>
 
            <motion.div
              className="h-[52px] w-full overflow-hidden rounded-xl relative"
              animate={matchAnimation}
            >
              <motion.input
                className={cn(
                  "h-full w-full rounded-xl border-2 px-3.5 py-3 tracking-[.59rem] outline-none placeholder:tracking-widest focus:border-foreground-muted"
                )}
                type="password"
                placeholder="Confirm Password"
                value={confirmPassword}
                onChange={handleConfirmPasswordChange}
                onFocus={() => setShowConfirm(true)}
                onBlur={() => {
                  if (!passwordsMatch) setShowConfirm(false);
                }}
                animate={borderAnimation}
              />
              <AnimatePresence>
                {passwordsMatch && (
                  <motion.div
                    className="absolute right-2 top-0 bottom-0 my-auto size-6 flex items-center justify-center rounded-full bg-green-500"
                    initial={{ opacity: 0, scale: 0 }}
                    animate={{ opacity: 1, scale: 1 }}
                    exit={{ opacity: 0, scale: 0 }}
                    transition={{ duration: 0.3 }}
                  >
                    {<Check className="size-4 text-green-500" />}
                  </motion.div>
                )}
              </AnimatePresence>
            </motion.div>
          </div>
        </div>
      </div>
    </main>
  );
};
 
export default Verifier;

Features

  • Real-time Validation: Users get immediate feedback on password confirmation.
  • Animations: Includes smooth animations for shaking and scaling using framer-motion.
  • Dynamic Visual Feedback: Password matching and mismatch are visually indicated with color changes.
  • Focus Management: Automatically toggles focus to guide the user during input.

Usage

To integrate the Verifier component into your project:

Installation

Make sure you have the necessary dependencies installed:

npm install framer-motion

or with pnpm:

pnpm add framer-motion

Component Setup

Import and use the Verifier component in your application:

"use client";
 
import Verifier from "./Verifier";
 
const App = () => {
  return (
    <div className="app">
      <Verifier />
    </div>
  );
};
 
export default App;

How It Works

  1. The user types their password in the first input.
  2. When the second (confirm password) input is focused, a div with visual placeholders shows the password pattern.
  3. As the user types in the confirmation field:
    • Matching letters are highlighted in green.
    • Mismatched letters are highlighted in red.
    • A "shake" animation is triggered if the user enters additional characters when the passwords don't match.
  4. If the passwords match, the border of the input turns green, indicating success.

Improvements You Can Make

Here are some ways to extend and improve the Verifier component:

1. With Zod

"use client";
 
import { motion } from "framer-motion";
import { useState } from "react";
import { z } from "zod";
import { cn } from "@/lib/utils";
 
const formSchema = z
  .object({
    password: z.string().min(6, "Password must be at least 6 characters"),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords must match",
    path: ["confirmPassword"], // Show the error on confirmPassword field
  });
 
const Verifier = () => {
  const [formData, setFormData] = useState({
    password: "",
    confirmPassword: "",
  });
 
  const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({});
  const [shake, setShake] = useState(false);
  const [showConfirm, setShowConfirm] = useState(false);
 
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
    setFormErrors((prev) => ({ ...prev, [name]: "" }));
  };
 
  const validateForm = () => {
    try {
      formSchema.parse(formData);
      setFormErrors({});
    } catch (error) {
      if (error instanceof z.ZodError) {
        const errors: { [key: string]: string } = {};
        error.errors.forEach((err) => {
          if (err.path[0]) errors[err.path[0] as string] = err.message;
        });
        setFormErrors(errors);
      }
    }
  };
 
  const bounceAnimation = {
    x: shake ? [-10, 10, -10, 10, 0] : 0,
    transition: { duration: 0.5 },
  };
 
  const getLetterStatus = (letter: string, index: number) => {
    if (!formData.confirmPassword[index]) return "";
    return formData.confirmPassword[index] === letter
      ? "bg-green-500/30"
      : "bg-red-500/30";
  };
 
  const matchAnimation = {
    scale: formData.password === formData.confirmPassword ? [1, 1.05, 1] : 1,
    transition: { duration: 0.3 },
  };
 
  const borderAnimation = {
    borderColor:
      formData.password === formData.confirmPassword ? "#10B981" : "",
    transition: { duration: 0.3 },
  };
 
  const handleBlur = () => {
    validateForm();
  };
 
  return (
    <main className="relative flex size-full w-full items-start justify-center px-4 py-10 md:items-center">
      <div className="z-10 flex w-full flex-col items-center">
        <div className="mx-auto flex h-full w-full max-w-lg flex-col items-center justify-center gap-8 p-16">
          <div className="relative flex w-full flex-col items-start justify-center gap-4">
            {/* Password input */}
            <div className="w-full">
              {!showConfirm ? (
                <motion.input
                  className="h-[52px] w-full rounded-xl border-2 px-3.5 py-3 tracking-[.59rem] outline-none placeholder:tracking-widest focus:border-foreground-muted"
                  type="password"
                  name="password"
                  placeholder="Enter Password"
                  value={formData.password}
                  onChange={handleInputChange}
                  onBlur={handleBlur}
                />
              ) : (
                <motion.div
                  className={cn(
                    "h-[52px] w-full rounded-xl border-2 px-2 py-2 bg-muted/40"
                  )}
                  animate={{
                    ...bounceAnimation,
                    ...matchAnimation,
                    ...borderAnimation,
                  }}
                >
                  <div className="relative h-full w-fit overflow-hidden rounded-lg ">
                    <div className="z-10 flex h-full items-center justify-center px-0 py-1">
                      {formData.password.split("").map((_, index) => (
                        <div
                          key={index}
                          className="flex h-full w-4 shrink-0 items-center justify-center"
                        >
                          <span className="size-[5px] rounded-full bg-muted-foreground" />
                        </div>
                      ))}
                    </div>
 
                    <div className="absolute bottom-0 left-0 top-0 z-0 flex h-full w-full items-center justify-start">
                      {formData.password.split("").map((letter, index) => (
                        <motion.div
                          key={index}
                          className={cn(
                            "ease absolute h-full w-4 transition-all duration-300",
                            getLetterStatus(letter, index)
                          )}
                          style={{
                            left: `${index * 16}px`,
                            scaleX: formData.confirmPassword[index] ? 1 : 0,
                            transformOrigin: "left",
                          }}
                        ></motion.div>
                      ))}
                    </div>
                  </div>
                </motion.div>
              )}
            </div>
 
            {/* Confirm password input */}
            <motion.div
              className="h-[52px] w-full overflow-hidden rounded-xl"
              animate={matchAnimation}
            >
              <motion.input
                className={cn(
                  "h-full w-full rounded-xl border-2 px-3.5 py-3 tracking-[.59rem] outline-none placeholder:tracking-widest focus:border-foreground-muted",
                  {
                    "border-red-500": formErrors.confirmPassword,
                    "border-green-500":
                      formData.confirmPassword &&
                      formData.password === formData.confirmPassword,
                    "border-gray-300":
                      !formErrors.confirmPassword &&
                      (!formData.confirmPassword ||
                        formData.password !== formData.confirmPassword),
                  }
                )}
                type="password"
                name="confirmPassword"
                placeholder="Confirm Password"
                value={formData.confirmPassword}
                onChange={handleInputChange}
                onFocus={() => setShowConfirm(true)}
                onBlur={handleBlur} // Trigger validation on blur
              />
            </motion.div>
            {formErrors.confirmPassword && (
              <p className="text-red-500">{formErrors.confirmPassword}</p>
            )}
          </div>
        </div>
      </div>
    </main>
  );
};
 
export default Verifier;

2. Add Password Strength Indicator

  • Show the strength of the password using a progress bar or color gradient.
  • Example:
    const getPasswordStrength = (password) => {
      if (password.length < 6) return "Weak";
      if (password.length < 10) return "Medium";
      return "Strong";
    };

3. Password Visibility Toggle

  • Add an icon/button to toggle between showing and hiding the password.
  • Example:
    const [showPassword, setShowPassword] = useState(false);
    <button onClick={() => setShowPassword(!showPassword)}>👁</button>;

4. Accessibility Enhancements

  • Add aria-live regions for error feedback.

Conclusion

The Verifier component provides an engaging way to handle password confirmation with dynamic animations and validation. It's an excellent starting point for enhancing user experience in forms, and with the suggested improvements, you can take it to the next level!

  • Use aria-invalid on the confirm password field when there's a mismatch.

4. Improved Error Messaging

  • Display specific error messages (e.g., "Passwords do not match" or "Password too short").

5. Responsive Design

  • Use em or rem units for better adaptability to various screen sizes.
  • Test on different devices to ensure a smooth experience.

6. Add Form Submission

  • Incorporate a submission handler to process the form data after successful validation.
  • Example:
    const handleSubmit = () => {
      if (passwordsMatch) {
        console.log("Passwords match! Submitting form...");
      } else {
        console.log("Fix errors before submitting.");
      }
    };

Credits

Built by Bossadi Zenith

inspired by Ln-ui and Preet