Forgot Password 1
Centered reset-request card: email with inline validation and a pending submit, swapping to a check-your-inbox state that echoes the address with resend and back-to-sign-in links.
Preview
Installation
npx shadcn@latest add https://hirael.com/r/forgot-password-01.jsonCode
components/blocks/forgot-password-01.tsx
"use client";
import * as React from "react";
import { ArrowRight, Loader2, MailCheck } from "lucide-react";
import { Button } from "@/registry/hirael/ui/button";
import { Input } from "@/registry/hirael/ui/input";
import { Label } from "@/registry/hirael/ui/label";
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function BrandMark({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 80 100"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
className={className}
>
<path d="M16 78 V40 a24 24 0 0 1 48 0 V78" />
<path d="M40 44 L43.2 52 L51 55 L43.2 58 L40 66 L36.8 58 L29 55 L36.8 52 Z" />
<path d="M22 86 H58" opacity="0.7" />
<path d="M28 92 H52" opacity="0.45" />
<path d="M34 96 H46" opacity="0.25" />
</svg>
);
}
export default function ForgotPassword01() {
const [email, setEmail] = React.useState("");
const [error, setError] = React.useState<string | null>(null);
const [status, setStatus] = React.useState<"idle" | "sending" | "sent">(
"idle",
);
const send = async () => {
setStatus("sending");
await new Promise((r) => setTimeout(r, 900));
setStatus("sent");
};
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email.trim()) {
setError("Enter the email you signed up with.");
return;
}
if (!EMAIL_PATTERN.test(email)) {
setError("That doesn't look like a valid email.");
return;
}
setError(null);
await send();
};
return (
<section className="relative isolate flex min-h-[640px] items-center justify-center bg-background py-16 md:py-24">
<div
aria-hidden
className="pointer-events-none absolute inset-0 -z-10 opacity-[0.35] [mask-image:radial-gradient(ellipse_at_center,black_30%,transparent_75%)]"
style={{
backgroundImage:
"linear-gradient(to right, var(--border) 1px, transparent 1px), linear-gradient(to bottom, var(--border) 1px, transparent 1px)",
backgroundSize: "32px 32px",
}}
/>
<div className="mx-auto w-full max-w-md px-6">
<div
className="rounded-sm border border-border bg-card"
style={{ boxShadow: "8px 8px 0 0 var(--border)" }}
>
{status === "sent" ? (
<div
role="status"
aria-live="polite"
className="flex flex-col items-center gap-4 px-8 py-10 text-center"
>
<span className="inline-flex size-10 items-center justify-center rounded-sm border border-border bg-background text-foreground">
<MailCheck className="size-5" />
</span>
<div className="flex flex-col gap-1">
<h1 className="font-serif text-3xl font-medium tracking-tight">
Check your inbox
</h1>
<p className="text-xs text-muted-foreground">
We sent a reset link to{" "}
<span className="font-mono text-foreground">{email}</span>. It
expires in 15 minutes.
</p>
</div>
<div className="mt-2 flex flex-col items-center gap-2">
<p className="text-xs text-muted-foreground">
Didn't get it?{" "}
<button
type="button"
onClick={send}
className="font-medium text-foreground underline-offset-4 hover:underline"
>
Resend
</button>
</p>
<a
href="#"
className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground transition-colors hover:text-foreground"
>
Back to sign in
</a>
</div>
</div>
) : (
<>
<div className="flex flex-col items-center gap-4 border-b border-border px-8 pb-6 pt-8">
<div className="flex size-10 items-center justify-center rounded-sm border border-border bg-background">
<BrandMark className="size-6 text-foreground" />
</div>
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="font-serif text-3xl font-medium tracking-tight">
Forgot your password?
</h1>
<p className="text-xs text-muted-foreground">
Enter your email and we'll send you a link to reset it.
</p>
</div>
</div>
<form
noValidate
className="flex flex-col gap-5 p-8"
onSubmit={onSubmit}
>
<div className="grid gap-1.5">
<Label
htmlFor="forgot01-email"
className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground"
>
Email
</Label>
<Input
id="forgot01-email"
type="email"
placeholder="you@studio.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
aria-invalid={Boolean(error) || undefined}
aria-describedby={
error ? "forgot01-email-error" : undefined
}
/>
{error ? (
<p
id="forgot01-email-error"
className="text-xs text-destructive"
>
{error}
</p>
) : null}
</div>
<Button
type="submit"
variant="default"
size="lg"
disabled={status === "sending"}
className="group"
>
{status === "sending" ? (
<>
<Loader2 className="size-4 animate-spin" />
Sending link…
</>
) : (
<>
Send reset link
<ArrowRight className="size-4 transition-transform duration-150 ease-out group-hover:translate-x-0.5 rtl:rotate-180 rtl:group-hover:-translate-x-0.5" />
</>
)}
</Button>
</form>
<div className="border-t border-border px-8 py-4 text-center">
<p className="text-xs text-muted-foreground">
Remembered it?{" "}
<a
href="#"
className="font-medium text-foreground underline-offset-4 hover:text-foreground hover:underline"
>
Back to sign in
</a>
</p>
</div>
</>
)}
</div>
<p className="mt-4 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">
Reset links expire after 15 minutes
</p>
</div>
</section>
);
}
Dependencies
shadcn registry
buttoninputlabel
npm
lucide-react