Forgot password
The password-reset request step: an icon badge, one email field, and a clear primary action on a light panel, next to a dark canvas carrying a customer testimonial. Kept deliberately minimal on the form side so it reads as a focused, low-friction step.
forgot-password.tsx
Built with
How it’s built
- 01One field, one action — resist adding anything that competes with the primary task.
- 02The testimonial panel is original, hand-written copy and an initials avatar, not a photo of a real person or a real customer quote — safe placeholder content for a pattern meant to be copy-pasted and re-branded.
- 03Same split rule as the other Auth patterns: real oks-ui components (the form) live on a plain light panel; the dark canvas carries only text we colour ourselves, since oks-ui's dark theme needs
data-theme="dark"on the document root to activate, not just a dark backdrop. - 04"Back to log in" is a real
Button variant="link"withstartContentfor the arrow icon — not a raw anchor. Passinghref(with notype) makesButtonrender as an<a>automatically. - 05"Forgot password?" is a real
PageTitle(safe to use its default styling here, since this panel is genuinely light and theme-driven); the shield icon badge above it and the "MK" initials avatar on the dark testimonial canvas are both realAvatarcomponents too — the badge isisBordered-free, using a plainclassNames={{ base: "border border-line bg-transparent" }}override for a simple outlined square instead of oks-ui's own multi-ring bordered style.
Source
Two files · copy & pasteStyling: this source uses Tailwind utility classes and this site’s colour tokens (for example text-ink, bg-paper and border-line). The oks-ui components render without them; swap those classes for your own.
import { Avatar } from "oks-ui/avatar";
import { Button } from "oks-ui/button";
import { Form, FormFieldSet } from "oks-ui/form-field-set";
import { PageTitle } from "oks-ui/page-title";
import { ShieldIcon } from "./icons";
const ArrowLeftIcon = ({ size = 16 }: { size?: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M13 8H3M3 8l4-4M3 8l4 4"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
const ForgotPassword = () => {
return (
<div className="@container w-full bg-paper-raised">
<div className="grid @2xl:min-h-[560px] @2xl:grid-cols-2">
{/* form panel */}
<div className="flex flex-col justify-center p-8 @2xl:p-14">
<div className="mx-auto flex w-full max-w-sm flex-col gap-6">
<span className="text-sm font-bold uppercase tracking-[0.2em] text-ink-faint">
Brand
</span>
<div className="flex flex-col gap-5">
<Avatar
icon={<ShieldIcon size={20} />}
radius="md"
size="lg"
classNames={{ base: "border border-line bg-transparent", icon: "text-accent" }}
/>
<PageTitle
as="h1"
title="Forgot password?"
subtitle="No worries, we'll send you reset instructions."
classNames={{
base: "flex-col items-start",
title: "text-[1.6rem] font-bold tracking-tight text-ink",
subtitle: "mt-1.5 text-[0.9rem] text-ink-muted",
}}
/>
</div>
<Form onSubmit={(data) => console.log(data)} className="flex flex-col gap-4">
<FormFieldSet
type="email"
name="email"
label="Email"
placeholder="Enter your email"
validation={{ rules: { required: true, email: true } }}
/>
<Button type="submit" color="primary" fullWidth>
Reset password
</Button>
</Form>
<Button
href="/sign-in"
variant="link"
size="sm"
startContent={<ArrowLeftIcon />}
className="h-auto justify-center p-0 text-[0.85rem] font-medium text-ink-muted no-underline"
>
Back to log in
</Button>
</div>
</div>
{/* testimonial panel — original copy, no photo of a real person */}
<div className="relative hidden flex-col overflow-hidden bg-[#04151a] p-12 @2xl:flex">
<div
aria-hidden="true"
className="absolute inset-0"
style={{
background:
"radial-gradient(120% 90% at 85% 15%, color-mix(in oklab, #0891b2 45%, transparent) 0%, transparent 55%), " +
"radial-gradient(90% 70% at 10% 90%, color-mix(in oklab, #0f766e 45%, transparent) 0%, transparent 60%), " +
"linear-gradient(160deg, #04151a 0%, #04262b 55%, #062f2c 100%)",
}}
/>
<div
aria-hidden="true"
className="absolute -right-20 top-1/4 h-72 w-72 rounded-full opacity-30 blur-3xl"
style={{ background: "#22d3ee" }}
/>
<div className="relative z-10 flex h-full flex-col justify-center gap-6">
<span className="text-[3rem] font-serif leading-none text-white/25" aria-hidden="true">
“
</span>
<p className="max-w-md text-[1.5rem] font-medium leading-snug tracking-tight text-white">
We cut our onboarding time in half within the first month —
support tickets dropped right along with it.
</p>
<div className="mt-2 flex items-center gap-3">
<Avatar
name="Maya Kessler"
radius="full"
classNames={{ base: "bg-white/10", name: "text-white" }}
/>
<div>
<p className="text-sm font-semibold text-white">Maya Kessler</p>
<p className="text-[0.8rem] text-white/60">
Head of Ops, Dyloc
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default ForgotPassword;It imports ./icons, so save this next to it:
export const ShieldIcon = ({ size = 18 }: { size?: number }) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 2 4 5v6c0 5 3.5 8.5 8 10 4.5-1.5 8-5 8-10V5l-8-3Z" />
<path d="m9 12 2 2 4-4" />
</svg>
);
};