Error Expired
Original · freelink/session expired page
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/error-expired.json"use client";
import {
useEffect,
useRef,
useState,
type ChangeEvent,
type FormEvent,
} from "react";
import { motion, useReducedMotion } from "motion/react";
type SendState = "idle" | "sending" | "sent";
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValidEmail = (value: string): boolean => EMAIL_RE.test(value.trim());
const RESEND_COOLDOWN = 45;
export default function ErrorExpired() {
const reduce = useReducedMotion();
const [email, setEmail] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const [status, setStatus] = useState<SendState>("idle");
const [sentTo, setSentTo] = useState<string>("");
const [cooldown, setCooldown] = useState<number>(0);
const [showReasons, setShowReasons] = useState<boolean>(false);
const sendTimerRef = useRef<number | null>(null);
useEffect(() => {
return () => {
if (sendTimerRef.current !== null) window.clearTimeout(sendTimerRef.current);
};
}, []);
// Single interval that runs only while a cooldown is active.
useEffect(() => {
if (cooldown <= 0) return;
const id = window.setInterval(() => {
setCooldown((current) => {
if (current <= 1) {
window.clearInterval(id);
return 0;
}
return current - 1;
});
}, 1000);
return () => window.clearInterval(id);
// Re-run only when crossing the active/inactive boundary.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cooldown === 0]);
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
setEmail(event.target.value);
if (error) setError(null);
};
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();
if (status === "sending" || cooldown > 0) return;
if (!isValidEmail(email)) {
setError("Enter a valid email address so we can reach you.");
return;
}
setError(null);
setStatus("sending");
sendTimerRef.current = window.setTimeout(() => {
setSentTo(email.trim());
setStatus("sent");
setCooldown(RESEND_COOLDOWN);
}, 1150);
};
const busy = status === "sending";
const buttonLabel =
busy
? "Sending secure link…"
: status === "sent"
? cooldown > 0
? `Resend available in ${cooldown}s`
: "Resend the link"
: "Email me a new link";
const buttonDisabled = busy || cooldown > 0;
const grains = [
{ cx: 92, cy: 148, delay: "0s" },
{ cx: 104, cy: 152, delay: "0.5s" },
{ cx: 100, cy: 143, delay: "1s" },
];
const focusRing =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950";
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-20 lg:px-8 lg:py-24 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes expr-spin { to { transform: rotate(360deg); } }
@keyframes expr-pulse {
0%, 100% { opacity: .30; transform: scale(1); }
50% { opacity: .55; transform: scale(1.06); }
}
@keyframes expr-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes expr-grain {
0%, 100% { transform: translateY(0); opacity: .55; }
50% { transform: translateY(2.5px); opacity: .95; }
}
@keyframes expr-sweep {
0% { transform: translateX(-130%) skewX(-16deg); }
100% { transform: translateX(230%) skewX(-16deg); }
}
.expr-spin { animation: expr-spin 22s linear infinite; }
.expr-pulse { animation: expr-pulse 4s ease-in-out infinite; }
.expr-float { animation: expr-float 6s ease-in-out infinite; }
.expr-grain { animation: expr-grain 2.6s ease-in-out infinite; }
.expr-sweep { animation: expr-sweep 6s linear infinite; }
@media (prefers-reduced-motion: reduce) {
.expr-spin, .expr-pulse, .expr-float, .expr-grain, .expr-sweep {
animation: none !important;
}
}
`}</style>
{/* Ambient background */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="absolute -left-24 top-8 h-72 w-72 rounded-full bg-amber-300/25 blur-3xl dark:bg-amber-500/10" />
<div className="absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-indigo-300/25 blur-3xl dark:bg-indigo-500/10" />
<div
className="absolute inset-0 opacity-[0.4] dark:opacity-[0.25]"
style={{
backgroundImage:
"linear-gradient(to right, rgba(100,116,139,0.10) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,116,139,0.10) 1px, transparent 1px)",
backgroundSize: "56px 56px",
maskImage: "radial-gradient(ellipse 70% 60% at 50% 40%, black, transparent)",
WebkitMaskImage: "radial-gradient(ellipse 70% 60% at 50% 40%, black, transparent)",
}}
/>
</div>
<div className="relative mx-auto grid w-full max-w-5xl items-center gap-10 lg:grid-cols-[0.85fr_1.15fr] lg:gap-16">
{/* Visual column */}
<motion.div
initial={reduce ? false : { opacity: 0, scale: 0.94 }}
animate={reduce ? {} : { opacity: 1, scale: 1 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="relative mx-auto flex w-full max-w-xs flex-col items-center lg:max-w-none"
>
<div className="relative aspect-square w-64 sm:w-72">
{/* Pulsing halo */}
<div className="expr-pulse absolute inset-0 rounded-full bg-gradient-to-br from-amber-400/40 to-rose-400/30 blur-2xl dark:from-amber-500/25 dark:to-rose-500/20" />
<div className="expr-float absolute inset-0">
<svg
viewBox="0 0 200 200"
className="h-full w-full drop-shadow-xl"
role="img"
aria-label="An hourglass with all of the sand run out to the bottom"
>
<defs>
<linearGradient id="expr-sand" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#fcd34d" />
<stop offset="55%" stopColor="#f59e0b" />
<stop offset="100%" stopColor="#d97706" />
</linearGradient>
<linearGradient id="expr-glass" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#e2e8f0" stopOpacity="0.55" />
<stop offset="100%" stopColor="#94a3b8" stopOpacity="0.18" />
</linearGradient>
</defs>
{/* Rotating dashed time ring */}
<circle
className="expr-spin"
cx="100"
cy="100"
r="90"
fill="none"
stroke="#f59e0b"
strokeWidth="1.5"
strokeDasharray="3 11"
strokeLinecap="round"
opacity="0.7"
style={{ transformOrigin: "100px 100px", animationDuration: "22s" }}
/>
{/* Frame caps */}
<rect x="42" y="34" width="116" height="11" rx="5.5" fill="#64748b" />
<rect x="42" y="155" width="116" height="11" rx="5.5" fill="#64748b" />
{/* Upper chamber (empty) */}
<path
d="M52 45 L148 45 L100 100 Z"
fill="url(#expr-glass)"
stroke="#94a3b8"
strokeWidth="2.5"
strokeLinejoin="round"
/>
{/* Lower chamber (glass) */}
<path
d="M100 100 L148 155 L52 155 Z"
fill="url(#expr-glass)"
stroke="#94a3b8"
strokeWidth="2.5"
strokeLinejoin="round"
/>
{/* Settled sand in the lower chamber */}
<path
d="M56 153 L144 153 L118 121 Q100 110 82 121 Z"
fill="url(#expr-sand)"
/>
<path
d="M56 153 L144 153 L118 121 Q100 110 82 121 Z"
fill="none"
stroke="#b45309"
strokeWidth="1.4"
strokeOpacity="0.35"
strokeLinejoin="round"
/>
{/* A few loose grains near the neck */}
{grains.map((g, i) => (
<circle
key={i}
className="expr-grain"
cx={g.cx}
cy={g.cy}
r="2.1"
fill="#fbbf24"
style={{ animationDelay: g.delay, animationDuration: "2.6s" }}
/>
))}
{/* Neck highlight */}
<circle cx="100" cy="100" r="3.4" fill="#cbd5e1" />
</svg>
</div>
</div>
<div className="mt-6 flex flex-col items-center gap-2 text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-amber-300/70 bg-amber-100/70 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-amber-800 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-300">
<span className="relative flex h-2 w-2">
<span className="expr-pulse absolute inline-flex h-full w-full rounded-full bg-amber-500" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-amber-500" />
</span>
Link expired
</span>
<p className="font-mono text-xs text-slate-500 dark:text-slate-400">
Reference EXP-7F3A · lapsed 18 minutes ago
</p>
</div>
</motion.div>
{/* Content card */}
<motion.div
initial={reduce ? false : { opacity: 0, y: 20 }}
animate={reduce ? {} : { opacity: 1, y: 0 }}
transition={{ duration: 0.55, ease: "easeOut", delay: reduce ? 0 : 0.12 }}
className="relative overflow-hidden rounded-3xl border border-slate-200 bg-white/90 p-6 shadow-2xl shadow-slate-900/5 backdrop-blur-sm sm:p-9 dark:border-slate-800 dark:bg-slate-900/80 dark:shadow-black/40"
>
{/* Top sheen sweep */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="expr-sweep absolute -top-1 left-0 h-px w-1/2 bg-gradient-to-r from-transparent via-indigo-400/70 to-transparent" />
</div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
Session security
</p>
<h1 className="mt-3 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
Your sign-in link has expired
</h1>
<p className="mt-4 max-w-prose text-base leading-relaxed text-slate-600 dark:text-slate-300">
For your protection, magic links stay valid for just{" "}
<span className="font-semibold text-slate-800 dark:text-slate-100">15 minutes</span>.
This one was opened after it lapsed, so it can no longer verify your account. Request a
fresh link below and you’ll be back in seconds.
</p>
<form onSubmit={handleSubmit} noValidate className="mt-7">
<label
htmlFor="expr-email"
className="block text-sm font-medium text-slate-700 dark:text-slate-200"
>
Where should we send the new link?
</label>
<div className="mt-2 flex flex-col gap-3 sm:flex-row">
<div className="relative flex-1">
<span aria-hidden="true" className="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500">
<svg viewBox="0 0 24 24" fill="none" className="h-5 w-5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="5" width="18" height="14" rx="2.5" />
<path d="m3.5 7 8.5 6 8.5-6" />
</svg>
</span>
<input
id="expr-email"
type="email"
inputMode="email"
autoComplete="email"
placeholder="you@company.com"
value={email}
onChange={handleChange}
aria-invalid={error ? true : undefined}
aria-describedby={error ? "expr-email-error" : "expr-email-hint"}
className={`w-full rounded-xl border bg-white py-3 pl-11 pr-3 text-sm text-slate-900 placeholder:text-slate-400 transition-colors dark:bg-slate-950/60 dark:text-slate-100 dark:placeholder:text-slate-500 ${focusRing} ${
error
? "border-rose-400 dark:border-rose-500/70"
: "border-slate-300 hover:border-slate-400 dark:border-slate-700 dark:hover:border-slate-600"
}`}
/>
</div>
<button
type="submit"
disabled={buttonDisabled}
className={`inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 transition-colors hover:bg-indigo-500 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:text-slate-500 disabled:shadow-none dark:disabled:bg-slate-700 dark:disabled:text-slate-400 ${focusRing}`}
>
{busy && (
<svg viewBox="0 0 24 24" className="expr-spin h-4 w-4" fill="none" style={{ animationDuration: "0.8s" }} aria-hidden="true">
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="3" opacity="0.3" />
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
</svg>
)}
{buttonLabel}
</button>
</div>
{error ? (
<p
id="expr-email-error"
role="alert"
className="mt-2 flex items-center gap-1.5 text-sm font-medium text-rose-600 dark:text-rose-400"
>
<svg viewBox="0 0 20 20" className="h-4 w-4 shrink-0" fill="currentColor" aria-hidden="true">
<path fillRule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm1-11a1 1 0 1 0-2 0v4a1 1 0 1 0 2 0V7Zm-1 7.5a1.1 1.1 0 1 0 0-2.2 1.1 1.1 0 0 0 0 2.2Z" clipRule="evenodd" />
</svg>
{error}
</p>
) : (
<p id="expr-email-hint" className="mt-2 text-xs text-slate-500 dark:text-slate-400">
We’ll never share your address. New links expire 15 minutes after they arrive.
</p>
)}
{/* Live success region */}
<div aria-live="polite" className="mt-4">
{status === "sent" && (
<div className="flex items-start gap-3 rounded-xl border border-emerald-300/70 bg-emerald-50 p-3.5 text-sm text-emerald-800 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-300">
<svg viewBox="0 0 24 24" className="mt-0.5 h-5 w-5 shrink-0" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M20 6 9 17l-5-5" />
</svg>
<span>
A new link is on its way to{" "}
<span className="font-semibold">{sentTo}</span>. It usually lands within a
minute — check spam if it hasn’t arrived.
</span>
</div>
)}
</div>
</form>
{/* Secondary actions */}
<div className="mt-7 flex flex-wrap items-center gap-x-6 gap-y-3 border-t border-slate-200 pt-6 text-sm dark:border-slate-800">
<button
type="button"
className={`inline-flex items-center gap-1.5 rounded-md font-semibold text-slate-700 transition-colors hover:text-indigo-600 dark:text-slate-200 dark:hover:text-indigo-400 ${focusRing}`}
>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M19 12H5" />
<path d="m12 19-7-7 7-7" />
</svg>
Back to sign in
</button>
<a
href="mailto:support@example.com"
className={`inline-flex items-center gap-1.5 rounded-md font-medium text-slate-500 transition-colors hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100 ${focusRing}`}
>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M12 17h.01" />
<path d="M12 13.5a1.5 1.5 0 0 1 1-1.4 2.6 2.6 0 1 0-3.2-3.3" />
<circle cx="12" cy="12" r="9" />
</svg>
Contact support
</a>
</div>
{/* Why did this happen — disclosure */}
<div className="mt-5">
<button
type="button"
onClick={() => setShowReasons((prev) => !prev)}
aria-expanded={showReasons}
aria-controls="expr-reasons"
className={`inline-flex items-center gap-1.5 rounded-md text-sm font-medium text-slate-500 transition-colors hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100 ${focusRing}`}
>
<svg
viewBox="0 0 24 24"
className={`h-4 w-4 transition-transform duration-200 ${showReasons ? "rotate-90" : ""}`}
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m9 18 6-6-6-6" />
</svg>
Why did this happen?
</button>
{showReasons && (
<ul
id="expr-reasons"
className="mt-3 space-y-2 border-l-2 border-slate-200 pl-4 text-sm leading-relaxed text-slate-600 dark:border-slate-700 dark:text-slate-300"
>
<li>The link sat unopened for longer than its 15-minute window.</li>
<li>You already used it once — each link works a single time.</li>
<li>A newer link was requested, which retires every earlier one.</li>
<li>Your device clock is far out of sync with our servers.</li>
</ul>
)}
</div>
</motion.div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Error 404
Originalfriendly 404 page with illustration and CTA

Error 500
Original500 server error page

Error 403
Original403 access denied page

Error Offline
Originaloffline / no-connection state

Error Maintenance
Originalscheduled maintenance page

Error Coming Soon
Originalcoming soon page with countdown and email

Error Under Construction
Originalunder construction page

Error Search Empty
Originalno search results page

Error Generic
Originalgeneric error page with retry

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

