Installation
Run the following command mkdir -p components/inputs && touch components/inputs/verifier.tsx
Copy 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;
Copy
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
Copy Copy
or with pnpm
:
pnpm add framer-motion
Copy
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;
Copy
How It Works
The user types their password in the first input.
When the second (confirm password) input is focused, a div with visual placeholders shows the password pattern.
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.
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;
Copy
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" ;
};
Copy
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 >;
Copy
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.
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." );
}
};
Copy
Credits
Built by Bossadi Zenith
inspired by Ln-ui and Preet