# Reset Password

> Choose a new password with live requirements and a security panel.

A shadcn/ui block from the [@devsloka](https://ui.devsloka.in/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/devsloka/forgot-password-reset).

- Page, with a live preview: https://shadcn.exchange/r/devsloka/forgot-password-reset
- Screenshot: https://shadcn.exchange/shot/devsloka/forgot-password-reset/light.png
- Registry item: https://ui.devsloka.in/r/forgot-password-reset.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/devsloka/forgot-password-reset.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `input`, `label`

## Files

### `src/registry/blocks/auth/forgot-password-reset.tsx`

```tsx
"use client";

import {
  useEffect,
  useId,
  useRef,
  useState,
  type ComponentProps,
  type CSSProperties,
  type FormEvent,
  type Ref,
} from "react";
import { Check, Eye, EyeOff, Loader2 } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";

export type ResetPasswordValues = { password: string };

export type PasswordRequirement = {
  id: string;
  label: string;
  test: (password: string) => boolean;
};

export type ForgotPasswordResetProps = {
  /** Product name shown in the wordmark. */
  brand?: string;
  title?: string;
  description?: string;
  /** The account being reset, shown in mono under the heading. */
  account?: string;
  requirements?: PasswordRequirement[];
  /** Short security note in the side panel. */
  securityNote?: string;
  continueHref?: string;
  signInHref?: string;
  /** Called with the new password. Throw to show an error. */
  onSubmit?: (values: ResetPasswordValues) => void | Promise<void>;
  /** Side panel and primary button ink. Defaults to Devsloka blue. */
  accent?: string;
  /** Text colour on `accent`. Defaults to white. */
  accentForeground?: string;
} & Omit<ComponentProps<"section">, "onSubmit">;

const DEFAULT_REQUIREMENTS: PasswordRequirement[] = [
  { id: "length", label: "12 characters or more", test: (p) => p.length >= 12 },
  { id: "case", label: "Upper and lower case letters", test: (p) => /[a-z]/.test(p) && /[A-Z]/.test(p) },
  { id: "number", label: "At least one number", test: (p) => /\d/.test(p) },
  { id: "spaces", label: "No spaces at the start or end", test: (p) => p.length > 0 && p.trim() === p },
];

const DEFAULTS = {
  brand: "Larch",
  title: "Choose a new password",
  description:
    "You’ll use it to sign in on every device. Sessions on other devices end once it’s saved.",
  account: "priya@fieldwork.org",
  securityNote:
    "Reset links expire after 30 minutes · requested from Lisbon, Chrome on macOS",
  accent: "#0A6EFF",
  accentForeground: "#FFFFFF",
};

const defaultSubmit = () => new Promise<void>((resolve) => setTimeout(resolve, 900));

type Field = "password" | "confirm";
type Errors = Partial<Record<Field, string>>;

function validate(password: string, confirm: string, requirements: PasswordRequirement[]): Errors {
  const errors: Errors = {};
  if (!password) errors.password = "Enter a new password.";
  else if (requirements.some((r) => !r.test(password)))
    errors.password = "This password doesn’t meet every requirement yet.";
  if (!confirm) errors.confirm = "Type the password again to confirm it.";
  else if (password && confirm !== password) errors.confirm = "The passwords don’t match.";
  return errors;
}

function Wordmark({ name }: { name: string }) {
  return (
    <span className="inline-flex items-center gap-2 text-base font-semibold tracking-tight">
      <span aria-hidden="true" className="size-2.5 bg-current" />
      {name}
    </span>
  );
}

function KeyholeMark() {
  return (
    <svg viewBox="0 0 120 160" aria-hidden="true" className="h-32 w-auto">
      <rect x="0" y="0" width="120" height="160" fill="none" stroke="currentColor" strokeWidth="2" />
      <circle cx="60" cy="62" r="22" fill="currentColor" />
      <rect x="51" y="70" width="18" height="52" fill="currentColor" />
      <rect x="0" y="140" width="120" height="2" fill="currentColor" opacity="0.4" />
    </svg>
  );
}

type PasswordFieldProps = {
  id: string;
  label: string;
  value: string;
  onValueChange: (value: string) => void;
  onBlur: () => void;
  error?: string;
  describedBy?: string;
  inputRef?: Ref<HTMLInputElement>;
};

function PasswordField({
  id,
  label,
  value,
  onValueChange,
  onBlur,
  error,
  describedBy,
  inputRef,
}: PasswordFieldProps) {
  const [visible, setVisible] = useState(false);
  const errorId = `${id}-error`;
  const describedByIds = [describedBy, error ? errorId : undefined].filter(Boolean).join(" ");

  return (
    <div className="grid gap-2">
      <Label htmlFor={id}>{label}</Label>
      <div className="relative">
        <Input
          ref={inputRef}
          id={id}
          type={visible ? "text" : "password"}
          autoComplete="new-password"
          required
          value={value}
          onChange={(event) => onValueChange(event.target.value)}
          onBlur={onBlur}
          aria-invalid={error ? true : undefined}
          aria-describedby={describedByIds || undefined}
          className="pr-11"
        />
        <button
          type="button"
          onClick={() => setVisible((v) => !v)}
          aria-label={visible ? "Hide password" : "Show password"}
          aria-controls={id}
          className="text-muted-foreground hover:text-foreground absolute inset-y-0 right-0 flex w-10 items-center justify-center rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
        >
          {visible ? <EyeOff aria-hidden="true" className="size-4" /> : <Eye aria-hidden="true" className="size-4" />}
        </button>
      </div>
      {error ? (
        <p id={errorId} className="text-destructive text-xs">
          {error}
        </p>
      ) : null}
    </div>
  );
}

export function ForgotPasswordReset({
  brand = DEFAULTS.brand,
  title = DEFAULTS.title,
  description = DEFAULTS.description,
  account = DEFAULTS.account,
  requirements = DEFAULT_REQUIREMENTS,
  securityNote = DEFAULTS.securityNote,
  continueHref = "#",
  signInHref = "#",
  onSubmit = defaultSubmit,
  accent = DEFAULTS.accent,
  accentForeground = DEFAULTS.accentForeground,
  className,
  style,
  ...props
}: ForgotPasswordResetProps) {
  const id = useId();
  const titleId = `${id}-title`;
  const passwordId = `${id}-password`;
  const confirmId = `${id}-confirm`;
  const requirementsId = `${id}-requirements`;
  const formErrorId = `${id}-form-error`;

  const passwordRef = useRef<HTMLInputElement>(null);
  const confirmRef = useRef<HTMLInputElement>(null);
  const successHeadingRef = useRef<HTMLHeadingElement>(null);

  const [password, setPassword] = useState("");
  const [confirm, setConfirm] = useState("");
  const [errors, setErrors] = useState<Errors>({});
  const [formError, setFormError] = useState<string>();
  const [submitted, setSubmitted] = useState(false);
  const [status, setStatus] = useState<"idle" | "submitting" | "success">("idle");

  useEffect(() => {
    if (status === "success") successHeadingRef.current?.focus();
  }, [status]);

  const metCount = requirements.filter((r) => r.test(password)).length;

  const revalidate = () => {
    if (submitted) setErrors(validate(password, confirm, requirements));
  };

  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setSubmitted(true);
    setFormError(undefined);
    const nextErrors = validate(password, confirm, requirements);
    setErrors(nextErrors);
    if (nextErrors.password) return passwordRef.current?.focus();
    if (nextErrors.confirm) return confirmRef.current?.focus();

    setStatus("submitting");
    try {
      await onSubmit({ password });
      setStatus("success");
    } catch {
      setStatus("idle");
      setFormError(
        "We couldn’t save the new password. Your old one still works. Try again, or request a fresh link if this one has expired."
      );
    }
  };

  return (
    <section
      data-slot="forgot-password-reset"
      aria-labelledby={titleId}
      style={
        {
          "--block-accent": accent,
          "--block-accent-fg": accentForeground,
          ...style,
        } as CSSProperties
      }
      className={cn("w-full bg-background text-foreground", className)}
      {...props}
    >
      <div className="mx-auto max-w-6xl px-6 py-16 md:py-24">
        <div className="grid border md:grid-cols-12">
          <aside className="flex flex-col justify-between gap-12 bg-[var(--block-accent)] px-6 py-8 text-[var(--block-accent-fg)] md:col-span-4 md:px-8 md:py-10 lg:col-span-3">
            <Wordmark name={brand} />
            <KeyholeMark />
            <div>
              <p className="font-mono text-[11px] tracking-wider uppercase">Security note</p>
              <p className="mt-3 font-mono text-xs leading-relaxed">{securityNote}</p>
              <p className="mt-6 text-xs leading-relaxed">
                Didn’t ask for this?{" "}
                <a
                  href={signInHref}
                  className="rounded-sm underline underline-offset-4 outline-none focus-visible:ring-2 focus-visible:ring-[var(--block-accent-fg)]"
                >
                  Sign in
                </a>{" "}
                as usual and the link stops working.
              </p>
            </div>
          </aside>

          <div className="px-6 py-10 md:col-span-8 md:px-12 md:py-14 lg:col-span-9 lg:px-16">
            {status === "success" ? (
              <div className="max-w-md">
                <p className="text-muted-foreground font-mono text-xs tracking-wider uppercase">
                  Step 2 of 2 · Done
                </p>
                <h2
                  id={titleId}
                  ref={successHeadingRef}
                  tabIndex={-1}
                  className="mt-4 text-4xl font-semibold tracking-[-0.03em] outline-none md:text-5xl"
                >
                  Password updated
                </h2>
                <p className="text-muted-foreground mt-5 text-base leading-relaxed">
                  <span className="text-foreground font-mono text-sm">{account}</span> now signs in
                  with the new password. We signed you out everywhere else and sent a confirmation
                  email.
                </p>
                <Button
                  asChild
                  size="lg"
                  className="mt-10 rounded-none bg-[var(--block-accent)] text-[var(--block-accent-fg)] shadow-none hover:bg-[var(--block-accent)] hover:underline hover:underline-offset-4"
                >
                  <a href={continueHref}>Continue to {brand}</a>
                </Button>
              </div>
            ) : (
              <div className="max-w-md">
                <p className="text-muted-foreground font-mono text-xs tracking-wider uppercase">
                  Step 2 of 2
                </p>
                <h2
                  id={titleId}
                  className="mt-4 text-4xl font-semibold tracking-[-0.03em] text-balance md:text-5xl"
                >
                  {title}
                </h2>
                <p className="text-muted-foreground mt-5 text-base leading-relaxed">{description}</p>
                <p className="mt-3 font-mono text-sm">{account}</p>

                <form
                  noValidate
                  onSubmit={handleSubmit}
                  aria-busy={status === "submitting"}
                  aria-describedby={formError ? formErrorId : undefined}
                  className="mt-10 grid gap-6"
                >
                  {/* Hidden username helps password managers save the new password to the right account. */}
                  <input type="hidden" name="username" autoComplete="username" value={account} />

                  <PasswordField
                    id={passwordId}
                    label="New password"
                    value={password}
                    onValueChange={setPassword}
                    onBlur={revalidate}
                    error={errors.password}
                    describedBy={requirementsId}
                    inputRef={passwordRef}
                  />

                  <div id={requirementsId}>
                    <p className="text-muted-foreground font-mono text-[11px] tracking-wider uppercase">
                      Requirements <span className="tabular-nums">{metCount}/{requirements.length}</span>
                    </p>
                    <ul className="mt-3 grid gap-2 sm:grid-cols-2">
                      {requirements.map((requirement) => {
                        const met = requirement.test(password);
                        return (
                          <li
                            key={requirement.id}
                            className={cn(
                              "flex items-start gap-2 text-sm transition-colors motion-reduce:transition-none",
                              met ? "text-foreground" : "text-muted-foreground"
                            )}
                          >
                            <span
                              aria-hidden="true"
                              className={cn(
                                "mt-0.5 flex size-4 shrink-0 items-center justify-center border",
                                met
                                  ? "border-[var(--block-accent)] bg-[var(--block-accent)] text-[var(--block-accent-fg)]"
                                  : "border-border"
                              )}
                            >
                              {met ? <Check className="size-3" strokeWidth={3} /> : null}
                            </span>
                            {requirement.label}
                            <span className="sr-only">{met ? "(met)" : "(not met yet)"}</span>
                          </li>
                        );
                      })}
                    </ul>
                  </div>

                  <PasswordField
                    id={confirmId}
                    label="Confirm new password"
                    value={confirm}
                    onValueChange={setConfirm}
                    onBlur={revalidate}
                    error={errors.confirm}
                    inputRef={confirmRef}
                  />

                  {formError ? (
                    <p id={formErrorId} className="text-destructive border-destructive border-l-2 pl-3 text-sm">
                      {formError}
                    </p>
                  ) : null}

                  <div className="flex flex-wrap items-center justify-between gap-4 border-t pt-6">
                    <a
                      href={signInHref}
                      className="text-muted-foreground hover:text-foreground rounded-sm text-sm underline-offset-4 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
                    >
                      Back to sign in
                    </a>
                    <Button
                      type="submit"
                      size="lg"
                      disabled={status === "submitting"}
                      className="rounded-none bg-[var(--block-accent)] text-[var(--block-accent-fg)] shadow-none hover:bg-[var(--block-accent)] hover:underline hover:underline-offset-4"
                    >
                      {status === "submitting" ? (
                        <>
                          <Loader2 aria-hidden="true" className="animate-spin motion-reduce:animate-none" />
                          Saving password…
                        </>
                      ) : (
                        "Save new password"
                      )}
                    </Button>
                  </div>
                </form>
              </div>
            )}

            <p role="status" aria-live="polite" className="sr-only">
              {status === "success"
                ? "Password updated."
                : formError
                  ? "The new password was not saved."
                  : ""}
            </p>
          </div>
        </div>
      </div>
    </section>
  );
}
```
