Back to Forms
Components / Forms
Forgot Password Form
Free Forms React component — ForgotPasswordForm. MIT licensed, copy-paste ready for Next.js and Tailwind CSS.
Password reset request form with email validation, loading state, success screen, and back-to-login navigation.
Preview
Setup
How to use
Free for personal and commercial use. No UI attribution required. Include the MIT copyright notice when copying component source into your project. Read the MIT license.
- 1
Run in your terminal:
$npm install clsx tailwind-merge lucide-react - 2
Copy
lib/cn.tsbelow. Skip if you already havecn(). - 3
Copy the code below and create
components/forms/forgot-password-form.tsxin your project. - 4
Import and render:
Exampleimport { ForgotPasswordForm } from "@/components/forms/forgot-password-form";
lib/cn.ts
import { type ClassValue, clsx } from "clsx";import { twMerge } from "tailwind-merge";export function cn(...inputs: ClassValue[]) {return twMerge(clsx(inputs));}
components/forms/forgot-password-form.tsx
"use client";import {forwardRef,useCallback,useId,useState,type ComponentPropsWithoutRef,type FormEvent,type ReactNode,} from "react";import Link from "next/link";import { AlertCircle, ArrowLeft, ArrowRight, Check, KeyRound, Loader2 } from "lucide-react";import { cn } from "@/lib/cn";const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;type FormLinkProps = Readonly<{href: string;className?: string;children: ReactNode;}>;function isPlaceholderHref(href: string): boolean {return !href || href === "#";}function FormLink({ href, className, children }: FormLinkProps) {if (isPlaceholderHref(href)) {return (<button type="button" className={cn("cursor-pointer", className)}>{children}</button>);}return (<Link href={href} className={className}>{children}</Link>);}export type ForgotPasswordFormProps = Readonly<{title?: string;subtitle?: string;submitLabel?: string;successTitle?: string;successMessage?: string;backLabel?: string;backHref?: string;submitErrorMessage?: string;loading?: boolean;onSubmit?: (email: string) => void | Promise<void>;} & Omit<ComponentPropsWithoutRef<"form">, "onSubmit">>;export const ForgotPasswordForm = forwardRef<HTMLDivElement, ForgotPasswordFormProps>(function ForgotPasswordForm({className,title = "Reset your password",subtitle = "Enter the email linked to your account and we'll send a reset link.",submitLabel = "Send reset link",successTitle = "Check your inbox",successMessage = "If an account exists for that email, you'll receive reset instructions shortly.",backLabel = "Back to sign in",backHref = "/components/login-form",submitErrorMessage = "We couldn't send the reset email. Please try again.",loading = false,onSubmit,onReset,...props},ref,) {const formId = useId();const [email, setEmail] = useState("");const [error, setError] = useState("");const [submitError, setSubmitError] = useState("");const [submitting, setSubmitting] = useState(false);const [success, setSuccess] = useState(false);const [sentTo, setSentTo] = useState("");const busy = loading || submitting;const handleSubmit = useCallback(async (event: FormEvent<HTMLFormElement>) => {event.preventDefault();if (busy || success) return;const trimmed = email.trim();if (!trimmed) {setError("Email is required.");return;}if (!EMAIL_RE.test(trimmed)) {setError("Enter a valid email address.");return;}setError("");setSubmitError("");setSubmitting(true);try {await onSubmit?.(trimmed);setSentTo(trimmed);setSuccess(true);} catch {setSubmitError(submitErrorMessage);} finally {setSubmitting(false);}},[busy, email, onSubmit, submitErrorMessage, success],);const handleReset = useCallback((event: FormEvent<HTMLFormElement>) => {onReset?.(event);if (event.defaultPrevented) return;setEmail("");setError("");setSubmitError("");},[onReset],);if (success) {return (<divref={ref}data-slot="forgot-password-form"data-successrole="status"aria-live="polite"className={cn("w-full max-w-md rounded-3xl border border-neutral-200 bg-white p-6 font-sans shadow-sm md:p-8",className,)}><div className="mb-5 flex items-center gap-2 text-[11px] font-semibold tracking-[0.18em] text-emerald-700 uppercase"><Check size={15} strokeWidth={2} aria-hidden />Email dispatched</div><h2 className="font-serif text-3xl leading-none tracking-tight text-neutral-950">{successTitle}</h2><p className="mt-3 text-sm leading-6 text-neutral-500">{successMessage}</p>{sentTo ? (<p className="mt-4 border-l-2 border-emerald-500 bg-emerald-50 px-3 py-2.5 font-mono text-xs text-neutral-700">{sentTo}</p>) : null}<FormLinkhref={backHref}className="mt-6 inline-flex items-center gap-1.5 text-sm font-medium text-neutral-900 underline-offset-2 hover:underline"><ArrowLeft size={14} aria-hidden />{backLabel}</FormLink></div>);}return (<div ref={ref} className={cn("w-full max-w-md", className)}><formdata-slot="forgot-password-form"noValidateonSubmit={handleSubmit}onReset={handleReset}aria-busy={busy}className="rounded-3xl border border-neutral-200 bg-white p-6 font-sans shadow-sm md:p-8"{...props}><FormLinkhref={backHref}className="mb-5 inline-flex items-center gap-1.5 text-sm text-neutral-500 transition-colors hover:text-neutral-900"><ArrowLeft size={14} aria-hidden />{backLabel}</FormLink><div className="mb-7"><div className="mb-5 flex items-center gap-2 text-[11px] font-semibold tracking-[0.18em] text-neutral-500 uppercase"><KeyRound size={15} strokeWidth={1.75} aria-hidden />Account recovery</div><h2 className="font-serif text-3xl leading-none tracking-tight text-neutral-950">{title}</h2><p className="mt-3 max-w-sm text-sm leading-6 text-neutral-500">{subtitle}</p></div><div><label htmlFor={`${formId}-email`} className="mb-1.5 block text-sm font-medium text-neutral-900">Email address</label><inputid={`${formId}-email`}name="email"type="email"autoComplete="email"disabled={busy}value={email}aria-invalid={Boolean(error)}aria-describedby={error ? `${formId}-email-error` : undefined}onChange={(event) => {setEmail(event.target.value);setSubmitError("");if (error) setError("");}}className={cn("h-11 w-full rounded-md border bg-white px-3.5 text-sm outline-none ring-0 transition-colors focus:ring-0 disabled:bg-neutral-50",error ? "border-rose-300" : "border-neutral-200 focus:border-neutral-900",)}placeholder="you@company.com"/>{error ? (<p id={`${formId}-email-error`} role="alert" className="mt-1.5 text-xs text-rose-600">{error}</p>) : null}</div>{submitError ? (<divrole="alert"className="mt-5 flex items-start gap-2.5 border-l-2 border-rose-500 bg-rose-50 px-3 py-2.5 text-sm text-rose-700"><AlertCircle size={16} className="mt-0.5 shrink-0" aria-hidden /><span>{submitError}</span></div>) : null}<buttontype="submit"disabled={busy}className="mt-6 flex h-11 w-full cursor-pointer items-center justify-center gap-2 rounded-md bg-neutral-950 px-4 text-sm font-semibold text-white transition-colors hover:bg-neutral-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-900 disabled:cursor-not-allowed disabled:opacity-60">{busy ? <Loader2 size={16} className="animate-spin" aria-hidden /> : null}{busy ? "Sending…" : submitLabel}{!busy ? <ArrowRight size={15} aria-hidden /> : null}</button></form></div>);},);ForgotPasswordForm.displayName = "ForgotPasswordForm";