# ForgotPasswordForm

> Request a password reset link by email, then a Check your inbox state with a resend countdown and a change-email link; en and ar built in.

Source: https://docs.nasaqui.com/components/forgot-password-form

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/forgot-password-form.json
```

A single email field. After `onSubmit` resolves it swaps to a confirmation: "Check your inbox", the address, a
"Resend email" button that waits out a countdown, and a link to use a different address.

## When to use

- The first step of password recovery.

## When not to use

- Setting the new password: use `ResetPasswordForm`.
- One-time codes: use `VerifyOtpForm`.

## Import

```tsx
import { ForgotPasswordForm } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { ForgotPasswordForm } from "@fadymondy/nasaq/web";

export function Forgot() {
  return <ForgotPasswordForm onSubmit={async ({ email }) => { await api.sendReset(email); }} />;
}

declare const api: { sendReset(email: string): Promise<void> };
```

## Anatomy

```
ForgotPasswordForm               data-slot="forgot-password-form"
├─ request state                 form, noValidate
│  ├─ error summary              role="alert"
│  ├─ Field: email               autocomplete="email"
│  └─ submit Button
└─ sent state                    heading is focused
   ├─ "Check your inbox" + address
   ├─ Resend button              disabled during the countdown
   └─ "Use a different email"
```

## API

**ForgotPasswordForm**: every `div` prop except `onSubmit` and `children`, plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onSubmit` | `(values: { email }) => Promise<AuthSubmitResult> \| AuthSubmitResult` | required | Resolve with nothing to show the sent state. |
| `onResend` | `(values: { email }) => Promise<AuthSubmitResult> \| AuthSubmitResult` | `onSubmit` | Called by "Resend email". |
| `resendSeconds` | `number` | `30` | Wait before the first and each further resend. |
| `defaultEmail` | `string` | | Prefill. |
| `labels` | `Partial<ForgotPasswordFormLabels>` | English or Arabic | `sentBody` uses `{email}`, `resendIn` uses `{time}`. |

Tip: respond the same whether or not the account exists, so the form does not leak who is registered.

## Examples

**Shorter cooldown**

```tsx
import { ForgotPasswordForm } from "@fadymondy/nasaq/web";

export function Quick() {
  return <ForgotPasswordForm resendSeconds={10} onSubmit={async () => {}} />;
}
```

## Accessibility

- The form is `noValidate` and validates itself, so messages are yours to localise and read in one place. Every field
  has a visible `<label>`; an invalid field gets `aria-invalid` and its message is linked with `aria-describedby`.
- On a failed submit focus moves to the first invalid field. When the failure is for the whole form (`error`) the
  summary at the top of the form (`role="alert"`) is focused instead, so it is announced.
- The submit button shows a spinner and is `aria-busy` while `onSubmit` runs; a second submit is ignored.
- When the sent state appears its heading takes focus and is announced; the resend countdown is a live region
  updated once a second only for the visible text, and the button is disabled (not hidden) while waiting.

## RTL & i18n

- Built-in English and Arabic strings follow the Nasaq locale (`useNasaq`). Pass `labels` to change any of them or to
  add another language; keys you omit keep the built-in text.
- Layout uses logical properties, so it mirrors under `dir="rtl"` with no extra work.
- The confirmed address is shown `dir="ltr"` inside the Arabic sentence so it does not reorder.

## Styling & tokens

- Built from `Field`, `Input`, `Button` and `Alert`; tokens only.

## Do / Don't

- Do give the same answer for unknown emails.
- Do keep the resend cooldown.
- Don't tell people whether an account exists.
- Don't auto-resend.

## Related

- [`auth-layout`](https://docs.nasaqui.com/components/auth-layout)
- [`login-form`](https://docs.nasaqui.com/components/login-form)
- [`reset-password-form`](https://docs.nasaqui.com/components/reset-password-form)
- [`verify-otp-form`](https://docs.nasaqui.com/components/verify-otp-form)

## Lab

https://docs.nasaqui.com/?path=/docs/components-auth-forgot-password-form--docs

## Code

### React

```tsx
import { ForgotPasswordForm } from "@fadymondy/nasaq/web";

export function Forgot() {
  return <ForgotPasswordForm onSubmit={async ({ email }) => { await api.sendReset(email); }} />;
}

declare const api: { sendReset(email: string): Promise<void> };
```

### shadcn

```tsx
import { ForgotPasswordForm } from "@/components/ui/forgot-password-form";

export function Forgot() {
  return <ForgotPasswordForm onSubmit={async ({ email }) => { await api.sendReset(email); }} />;
}

declare const api: { sendReset(email: string): Promise<void> };
```

### Vue

```vue
<script setup lang="ts">
import { NqForgotPasswordForm, type ForgotPasswordValues } from "@fadymondy/nasaq/vue";

// Your API call. Resolve nothing and the form shows "Check your inbox" with a resend button on a cooldown.
async function sendReset({ email }: ForgotPasswordValues) {
  await new Promise((r) => setTimeout(r, 400));
  void email;
}
</script>

<template>
  <div class="w-80">
    <NqForgotPasswordForm :on-submit="sendReset" />
  </div>
</template>
```

### Blade

```blade
{{-- Your API call. Resolve nothing and the form shows "Check your inbox" with a resend button on a cooldown. --}}
<div class="w-80">
    <x-nq::forgot-password-form
        x-on:nq-forgot-password="$event.detail.waitUntil(new Promise((done) => setTimeout(done, 300)))" />
</div>
```

### HTML + Alpine

```html
<div class="w-80">
    <div data-slot="forgot-password-form" x-data="nqForgotPasswordForm(JSON.parse('{\u0022resendSeconds\u0022:30,\u0022names\u0022:[\u0022email\u0022],\u0022fieldLabels\u0022:{\u0022email\u0022:\u0022Email\u0022},\u0022errorTitle\u0022:\u0022Fix this to continue\u0022,\u0022failed\u0022:\u0022Something went wrong. Try again.\u0022,\u0022labels\u0022:{\u0022email\u0022:\u0022Email\u0022,\u0022emailPlaceholder\u0022:\u0022you@example.com\u0022,\u0022submit\u0022:\u0022Send reset link\u0022,\u0022emailRequired\u0022:\u0022Enter your email address.\u0022,\u0022emailInvalid\u0022:\u0022Enter a valid email address.\u0022,\u0022errorTitle\u0022:\u0022Fix this to continue\u0022,\u0022failed\u0022:\u0022Something went wrong. Try again.\u0022,\u0022sentTitle\u0022:\u0022Check your inbox\u0022,\u0022sentBody\u0022:\u0022If an account exists for {email}, we sent a link to reset the password.\u0022,\u0022resend\u0022:\u0022Resend email\u0022,\u0022resendIn\u0022:\u0022Resend in {time}\u0022,\u0022resent\u0022:\u0022We sent the email again.\u0022,\u0022changeEmail\u0022:\u0022Use a different email\u0022}}'))" x-bind:data-state="sentTo ? 'sent' : 'idle'"
    x-on:nq-forgot-password="$event.detail.waitUntil(new Promise((done) =&gt; setTimeout(done, 300)))" class="w-full">
    <form novalidate x-show="! sentTo" x-bind:aria-busy="pending ? 'true' : null" x-on:submit.prevent="onSubmit()" x-on:input="clear($event.target.name)"
         class="flex w-full flex-col gap-4">
        <div tabindex="-1" class="sr-only" x-show="! problems()"></div>
<div tabindex="-1" data-slot="auth-error-summary" class="outline-none" x-show="problems()" style="display: none">
    <div data-slot="alert" data-tone="danger" role="alert" class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-danger/30 bg-nq-danger-soft">
        <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>        <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
            <div data-slot="alert-title" class="text-label text-foreground" x-text="summaryTitle()"></div>
            <div data-slot="alert-description" class="text-body-sm text-muted-foreground" x-show="! error && entries().length" style="display: none">
                <ul class="flex list-disc flex-col gap-0.5 ps-4">
                    <template x-for="e in entries()" :key="e.name">
                        <li><button type="button" class="text-start underline underline-offset-2" x-on:click="focusField(e.name)" x-text="(e.label ? e.label + ': ' : '') + e.message"></button></li>
                    </template>
                </ul>
            </div>
        </div>
    </div>
</div>
        <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="bad.email" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  for="nq-forgot-000001-email" class="text-label text-foreground data-disabled:opacity-50">Email</label>
            <input data-slot="input" type="email"
     dir="ltr"      name="email"             id="nq-forgot-000001-email" autocomplete="email" inputmode="email" value="" placeholder="you@example.com" aria-describedby="nq-forgot-000001-email-error" x-bind:aria-invalid="bad.email ? &#039;true&#039; : null" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control text-start">
            <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  id="nq-forgot-000001-email-error" class="text-caption text-nq-danger-text"><span x-text="fe.email"></span></div>
</div>
        <button data-slot="button"
     type="submit"                         x-bind:disabled="pending" x-bind:data-disabled="pending ? &#039;&#039; : null" x-bind:aria-busy="pending ? &#039;true&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-[calc(var(--nq-control)+8px)] px-5 text-body">
        <template x-if="pending"><svg data-slot="spinner" aria-hidden="true" class="size-4 animate-spin motion-reduce:animate-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg></template>
            Send reset link</button>
    </form>
    <div class="flex w-full flex-col gap-4 text-start" x-show="sentTo" style="display: none">
        <div class="flex size-10 items-center justify-center rounded-full bg-nq-success-soft text-nq-success-text">
            <svg aria-hidden="true" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"/>
  <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
  <path d="m16 19 2 2 4-4"/>
</svg>        </div>
        <div role="status" class="flex flex-col gap-1.5">
            <h2 data-slot="forgot-password-sent-title" tabindex="-1" class="text-h3 text-foreground outline-none">Check your inbox</h2>
            <p class="text-body-sm text-muted-foreground">If an account exists for <bdi dir="ltr" class="font-medium text-foreground" x-text="sentTo"></bdi>, we sent a link to reset the password.</p>
        </div>
        <div data-slot="alert" data-tone="danger" role="alert"
        x-show="resendError" style="display: none" class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-danger/30 bg-nq-danger-soft">
    <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>    <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
                            <div data-slot="alert-description" class="text-body-sm text-foreground"><span x-text="resendError"></span></div>
            </div>
    </div>
        <span role="status" class="sr-only" x-text="resendDone && cooldown > 0 ? 'We sent the email again.' : ''"></span>
        <button data-slot="forgot-password-resend"
     type="button"                         x-on:click="resend()" x-bind:disabled="resendOff()" x-bind:data-disabled="resendOff() ? &#039;&#039; : null" x-bind:aria-busy="resendPending ? &#039;true&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-[calc(var(--nq-control)+8px)] px-5 text-body">
        <template x-if="resendPending"><svg data-slot="spinner" aria-hidden="true" class="size-4 animate-spin motion-reduce:animate-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg></template>
            <span x-text="resendLabel()">Resend email</span></button>
        <button data-slot="button"
     type="button"                         x-on:click="changeEmail()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground underline decoration-nq-line underline-offset-4 hover:decoration-current h-auto px-0 self-start">
        Use a different email</button>
    </div>
</div>
</div>
```
