Back to Dictionary
Empty & Error States
●○○Ready to Use
Recommended defaultForm Validation Error
Small red helper text that gracefully slides down from underneath an input field the moment the user tabs away with an invalid entry.
Use
Sign up forms
Avoid
Waiting until final submit (validate inline when possible)
Safer Alternative
No safer default listed.
Risk Level
Low risk
Live Demo
Subscribe
Enter your email to join our newsletter.
What It Is
Small red helper text that gracefully slides down from underneath an input field the moment the user tabs away with an invalid entry.
✓When to Use
- Sign up forms
- Checkouts
- Settings configurations
✕When NOT to Use
- Waiting until final submit (validate inline when possible)
Configuration Tips
- 01Animate height from 0 to auto, and opacity from 0 to 1
- 02Use a very fast duration (150ms)
You've Seen It In
Stripe CheckoutLinear LoginShopify
AI Implementation Prompts
Move from a fast scaffold to production details, then tune timing and edge states.
Make the inline red error message slide down smoothly under an input field when validation fails.
Reference Implementation
The same implementation approach used by the live preview, kept compact enough to inspect and adapt.
"use client";
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { AlertCircle, CheckCircle2 } from 'lucide-react';
export function FormValidationErrorDemo({ isPlaying = false }: { isPlaying?: boolean }) {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'error' | 'success'>('idle');
const [errorMsg, setErrorMsg] = useState('');
const validateEmail = (e: React.FormEvent) => {
e.preventDefault();
if (!email) {
setErrorMsg("Email address is required");
setStatus('error');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
setErrorMsg("Please enter a valid email address");
setStatus('error');
} else {
setStatus('success');
setErrorMsg('');
setTimeout(() => setStatus('idle'), 3000);
}
};
useEffect(() => {
if (!isPlaying) {
setEmail('');
setStatus('idle');
return;
}
const loop = async () => {
setEmail('');
setStatus('idle');
await new Promise(r => setTimeout(r, 800));
setEmail('invalid-email');
setErrorMsg("Please enter a valid email address");
setStatus('error');
await new Promise(r => setTimeout(r, 1500));
};
loop();
const interval = setInterval(loop, 3000);
return () => clearInterval(interval);
}, [isPlaying]);
return (
<div className="relative flex h-64 w-full items-center justify-center rounded-2xl bg-white p-6 shadow-sm ring-1 ring-stone-200">
<div className="w-full max-w-sm scale-90">
<div className="mb-6 text-center">
<h3 className="mb-2 text-xl font-bold text-stone-900">Subscribe</h3>
<p className="text-sm text-stone-500">Enter your email to join our newsletter.</p>
</div>
<form onSubmit={validateEmail} className="space-y-4">
<div className="space-y-2">
<label htmlFor="email" className="block text-sm font-medium text-stone-700">
Email Address
</label>
<div className="relative">
<motion.div
animate={status === 'error' ? { x: [-5, 5, -5, 5, -3, 3, 0] } : {}}
transition={{ duration: 0.4 }}
>
<input
id="email"
type="text"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (status === 'error') setStatus('idle');
}}
className={`w-full h-12 px-4 rounded-xl border-2 font-medium text-stone-900 outline-none transition-all ${status === 'error'
? 'border-red-500 bg-red-50/50 focus:border-red-600 focus:ring-4 focus:ring-red-500/10'
: status === 'success'
? 'border-green-500 bg-green-50/50 focus:border-green-600'
: 'border-stone-200 bg-white focus:border-stone-400 focus:ring-4 focus:ring-stone-400/10'
}`}
placeholder="you@example.com"
/>
</motion.div>
<AnimatePresence>
{status === 'error' && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="absolute right-4 top-3.5 text-red-500 pointer-events-none"
>
<AlertCircle className="w-5 h-5" />
</motion.div>
)}
{status === 'success' && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="absolute right-4 top-3.5 text-green-500 pointer-events-none"
>
<CheckCircle2 className="w-5 h-5" />
</motion.div>
)}
</AnimatePresence>
</div>
<AnimatePresence>
{status === 'error' && (
<motion.p
initial={{ opacity: 0, y: -5, height: 0 }}
animate={{ opacity: 1, y: 0, height: 'auto' }}
exit={{ opacity: 0, y: -5, height: 0 }}
className="text-sm font-medium text-red-500"
>
{errorMsg}
</motion.p>
)}
</AnimatePresence>
</div>
<button
type="submit"
className="w-full py-3.5 bg-stone-900 text-white rounded-xl font-medium hover:bg-stone-800 transition-colors shadow-sm focus:ring-4 focus:ring-stone-200 outline-none"
>
{status === 'success' ? 'Subscribed!' : 'Subscribe'}
</button>
</form>
</div>
</div>
);
}