# Recover access

Request a password reset, choose a new password and handle recovery states.

Import: @slices.sh/auth/headless

Public parts: 16

## Installation

### 1. Install the service registry

services/index.ts, which `import "@/services"` loads. A provider setup writes the same file; skip this if your app has one.

```bash
npx shadcn@latest add "https://slices.sh/r/services.json"
```

### 2. Install the headless library

```bash
npm install @slices.sh/auth@^0.1.10
```

### 3. Install the service

Install the provider configuration via shadcn or the slices CLI. Skip this step if your app already has the service configured. Provider commands and provisioning: https://slices.sh/docs/services/auth.md

## Anatomy

### Composition

```text
SignIn.Root
  SignIn.Viewport
    SignIn.Step name="identify"
      SignIn.ForgotPassword
    SignIn.Step name="reset-request"
      SignIn.ResetSent
        SignIn.ResetDestination
      SignIn.ResetMethod method="link"
        SignIn.RequestReset
      SignIn.BackToSignIn
    SignIn.Step name="reset-code"
      SignIn.ResetCodeInput
      SignIn.ResendResetCode
      SignIn.ResetMethod method="code"
    SignIn.Step name="reset"
      SignIn.ResetUnused
        SignIn.NewPassword
        SignIn.PasswordStrength
          SignIn.StrengthSegment
        SignIn.PasswordFeedback
        SignIn.PasswordRequirement
        SignIn.SubmitReset
      SignIn.ResetDone
```

## Minimal composition

```tsx
"use client"

import "@/services"
import { SignIn } from "@slices.sh/auth/headless"

export function Example() {
  return (
    <SignIn.Root>
      <SignIn.Step name="identify">
        <SignIn.ForgotPassword>Forgot password?</SignIn.ForgotPassword>
      </SignIn.Step>
      <SignIn.Step name="reset-request">
        <SignIn.Form>
          <SignIn.Email aria-label="Email" />
          <SignIn.RequestReset>Send reset link</SignIn.RequestReset>
          <SignIn.ResetSent>Check your inbox.</SignIn.ResetSent>
        </SignIn.Form>
      </SignIn.Step>
      <SignIn.Step name="reset">
        <SignIn.Form>
          <SignIn.NewPassword aria-label="New password" />
          <SignIn.PasswordStrength />
          <SignIn.SubmitReset>Save password</SignIn.SubmitReset>
        </SignIn.Form>
      </SignIn.Step>
      <SignIn.ResetDone>Password changed.</SignIn.ResetDone>
      <SignIn.Error />
    </SignIn.Root>
  )
}

```

Enable password-reset with a provider that sends reset links. The link returns to this page with a reset token.

## Complete API reference

### SignIn.PasswordStrength

content · Element: div

The meter over a new password: `data-level` is the score, 0 to 4, and absent until one is
known.

Absent over an empty field too. A meter that says "very weak" before anything has been
typed is a screen telling somebody off for not having started.

Import: @slices.sh/auth/headless

Capability: methods: password

PasswordStrength must be inside <SignIn.Root> or <SignUp.Root>.

```tsx
PartProps<"div", { score: number | null; levels: number }>
```

Alternate import: Strength from @slices.sh/auth/headless/password-settings

Alternate import: SignIn.PasswordStrength from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.PasswordStrength from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.PasswordStrength from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/password-strength.tsx

### SignIn.StrengthSegment

content · Element: span

One bar of the meter, repeated once per level.

The count comes from the estimator's own scale rather than from the card, so a skin
cannot draw five bars over a score that only goes to four.

Import: @slices.sh/auth/headless

Capability: methods: password

StrengthSegment must be inside <SignIn.Root> or <SignUp.Root>.

```tsx
Props

type Props = PartProps<"span", { index: number; filled: boolean }>
```

Alternate import: StrengthSegment from @slices.sh/auth/headless/password-settings

Alternate import: SignIn.StrengthSegment from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.StrengthSegment from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.StrengthSegment from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/strength-segment.tsx

### SignIn.PasswordFeedback

content · Element: p

What the estimator would say about this password, in its own words.

Its sentences come from the language pack, so they are already the person's language and
are not repeated in `copy`. Absent until there is something to say: a strong password
earns no advice, and an empty field has not been judged.

Import: @slices.sh/auth/headless

Capability: methods: password

PasswordFeedback must be inside <SignIn.Root> or <SignUp.Root>.

```tsx
PartProps<"p", { warning: string | null; suggestions: readonly string[] }>
```

Alternate import: Feedback from @slices.sh/auth/headless/password-settings

Alternate import: SignIn.PasswordFeedback from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.PasswordFeedback from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.PasswordFeedback from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/password-feedback.tsx

### SignIn.PasswordRequirement

content · Element: li

One rule a new password has to meet, said out loud rather than discovered by being
refused.

The numbers are the backend's, read from the capability, so the sentence and the refusal
can never disagree. `data-state` is `met` or `unmet`, which is what a skin ticks off. A
rule the backend does not have is absent: a "hard to guess" line over a backend that
accepts anything would be a promise nobody keeps.

Import: @slices.sh/auth/headless

Capability: methods: password

PasswordRequirement must be inside <SignIn.Root> or <SignUp.Root>.

```tsx
PartProps<"li", State> & { rule: PasswordRule }

export type PasswordRule = "length" | "strength"

type State = { met: boolean; minLength: number; maxLength: number }
```

Alternate import: Requirement from @slices.sh/auth/headless/password-settings

Alternate import: SignIn.PasswordRequirement from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.PasswordRequirement from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.PasswordRequirement from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/password-requirement.tsx

### SignIn.BackToSignIn

action · Element: button

Leaves the reset screens. It drops the token as well as the intent, which is what takes
a live credential out of the address bar once it has been spent, and the code challenge
with it: coming back to a half-typed code for a reset nobody is doing any more is a
screen nobody asked for.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.BackToSignIn must be inside <SignIn.Root>.

```tsx
PartProps<"button">
```

Alternate import: SignIn.BackToSignIn from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.BackToSignIn from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.BackToSignIn from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/back-to-sign-in.tsx

### SignIn.ForgotPassword

action · Element: button

Asks for the reset screen. Absent unless the backend can actually put a link in front of
somebody: a "Forgot password?" that leads nowhere is worse than none at all.

`stage="identify"` is for the link under the email field of an identifier-first card: it
is there only while that step is drawn. Where the email and the password stand together
there is no such step, and the password field's own link is the only one.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.ForgotPassword must be inside <SignIn.Root>.

```tsx
PartProps<"button"> & { stage?: "identify" }
```

Alternate import: SignIn.ForgotPassword from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.ForgotPassword from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.ForgotPassword from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/forgot-password.tsx

### SignIn.RequestReset

action · Element: button

ForgotPassword opens the reset steps, and nothing unless the backend can deliver a link or a code. ResetMethod gates on method="link" | "code". RequestReset sends it with { pending, method }; ResetSent says it went, the same for any address. ResetDestination, ResetCodeInput and ResendResetCode are the code variant. ResetUnused gates while a link is unspent; NewPassword and SubmitReset set the password; ResetDone follows. BackToSignIn leaves and drops the token from the address bar.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.RequestReset must be inside <SignIn.Root>.

```tsx
PartProps<"button", { pending: boolean; method: "link" | "code" }>
```

Alternate import: SignIn.RequestReset from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.RequestReset from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.RequestReset from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/request-reset.tsx

### SignIn.ResetMethod

gate

How this install replaces a forgotten password: a link to the inbox, or a code to type.

A gate, because the two are different screens rather than one screen with a different
sentence — a link is followed away and comes back with a token, a code is typed here —
and only the backend knows which it can deliver.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.ResetMethod must be inside <SignIn.Root>.

```tsx
GateProps & { method: "link" | "code" }
```

Alternate import: SignIn.ResetMethod from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.ResetMethod from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.ResetMethod from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/reset-method.tsx

### SignIn.ResetSent

gate

ForgotPassword opens the reset steps, and nothing unless the backend can deliver a link or a code. ResetMethod gates on method="link" | "code". RequestReset sends it with { pending, method }; ResetSent says it went, the same for any address. ResetDestination, ResetCodeInput and ResendResetCode are the code variant. ResetUnused gates while a link is unspent; NewPassword and SubmitReset set the password; ResetDone follows. BackToSignIn leaves and drops the token from the address bar.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.ResetSent must be inside <SignIn.Root>.

```tsx
GateProps<{ delivery: "mail" | "terminal" | "log" }>
```

Alternate import: SignIn.ResetSent from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.ResetSent from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.ResetSent from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/reset-sent.tsx

### SignIn.ResetDestination

content

Where the reset code went, as the backend spells it back: often masked, never guessed.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.ResetDestination must be inside <SignIn.Root>.

```tsx
PartProps<null>
```

Alternate import: SignIn.ResetDestination from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.ResetDestination from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.ResetDestination from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/reset-destination.tsx

### SignIn.ResetCodeInput

input · Element: input

The code that proves the address a reset was asked for.

The same six digits as a sign-in code and a different thing entirely: a right one answers
a token that sets a password, and signs nobody in. Checked the moment its last digit
lands, so there is no button beside it.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.ResetCodeInput must be inside <SignIn.Root>.

```tsx
PartProps<"input", { invalid: boolean }>
```

Alternate import: SignIn.ResetCodeInput from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.ResetCodeInput from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.ResetCodeInput from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/reset-code-input.tsx

### SignIn.ResendResetCode

action · Element: button

ForgotPassword opens the reset steps, and nothing unless the backend can deliver a link or a code. ResetMethod gates on method="link" | "code". RequestReset sends it with { pending, method }; ResetSent says it went, the same for any address. ResetDestination, ResetCodeInput and ResendResetCode are the code variant. ResetUnused gates while a link is unspent; NewPassword and SubmitReset set the password; ResetDone follows. BackToSignIn leaves and drops the token from the address bar.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.ResendResetCode must be inside <SignIn.Root>.

```tsx
PartProps<"button", { seconds: number; pending: boolean; exhausted: boolean }>
```

Alternate import: SignIn.ResendResetCode from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.ResendResetCode from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.ResendResetCode from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/resend-reset-code.tsx

### SignIn.ResetUnused

gate

ForgotPassword opens the reset steps, and nothing unless the backend can deliver a link or a code. ResetMethod gates on method="link" | "code". RequestReset sends it with { pending, method }; ResetSent says it went, the same for any address. ResetDestination, ResetCodeInput and ResendResetCode are the code variant. ResetUnused gates while a link is unspent; NewPassword and SubmitReset set the password; ResetDone follows. BackToSignIn leaves and drops the token from the address bar.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.ResetUnused must be inside <SignIn.Root>.

```tsx
GateProps
```

Alternate import: SignIn.ResetUnused from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.ResetUnused from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.ResetUnused from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/reset-unused.tsx

### SignIn.NewPassword

input · Element: input

The password being set by a reset, which is a new one and is announced as such.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.NewPassword must be inside <SignIn.Root>.

```tsx
PartProps<"input", { invalid: boolean }>
```

Alternate import: SignIn.NewPassword from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.NewPassword from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.NewPassword from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/new-password.tsx

### SignIn.SubmitReset

action · Element: button

ForgotPassword opens the reset steps, and nothing unless the backend can deliver a link or a code. ResetMethod gates on method="link" | "code". RequestReset sends it with { pending, method }; ResetSent says it went, the same for any address. ResetDestination, ResetCodeInput and ResendResetCode are the code variant. ResetUnused gates while a link is unspent; NewPassword and SubmitReset set the password; ResetDone follows. BackToSignIn leaves and drops the token from the address bar.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.SubmitReset must be inside <SignIn.Root>.

```tsx
PartProps<"button", { pending: boolean }>
```

Alternate import: SignIn.SubmitReset from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.SubmitReset from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.SubmitReset from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/submit-reset.tsx

### SignIn.ResetDone

gate

ForgotPassword opens the reset steps, and nothing unless the backend can deliver a link or a code. ResetMethod gates on method="link" | "code". RequestReset sends it with { pending, method }; ResetSent says it went, the same for any address. ResetDestination, ResetCodeInput and ResendResetCode are the code variant. ResetUnused gates while a link is unspent; NewPassword and SubmitReset set the password; ResetDone follows. BackToSignIn leaves and drops the token from the address bar.

Import: @slices.sh/auth/headless

Capability: password-reset

SignIn.ResetDone must be inside <SignIn.Root>.

```tsx
GateProps
```

Alternate import: SignIn.ResetDone from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.ResetDone from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.ResetDone from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/reset-done.tsx

### SignIn.Root

scope

Step gates on name and accepts render={<MotionPanel />} to animate; without render it is a wrapper-free gate. Status is the one polite live region, mounted for the card's life, that speaks waits and the attempt budget. Viewport is a plain container for animated steps. Error is <div role="alert"> with { code, message }, only for the current step's last failed action. RetryAfter gets { seconds } while a rate limit holds. Restart returns to identify and drops the challenge.

Import: @slices.sh/auth/headless

```tsx
SignInRootProps & AuthRootProps

type SignInRootProps = RootProps & {
  onSignedIn?: (destination: string) => void
  channels?: readonly OtpChannel[]
  flow?: IdentifierFlow
  emailPassword?: boolean
}
```

Alternate import: SignIn.Root from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.Root from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.Root from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/root.tsx

### SignIn.Viewport

content · Element: div

Step gates on name and accepts render={<MotionPanel />} to animate; without render it is a wrapper-free gate. Status is the one polite live region, mounted for the card's life, that speaks waits and the attempt budget. Viewport is a plain container for animated steps. Error is <div role="alert"> with { code, message }, only for the current step's last failed action. RetryAfter gets { seconds } while a rate limit holds. Restart returns to identify and drops the challenge.

Import: @slices.sh/auth/headless

```tsx
PartProps<"div">
```

Alternate import: SignIn.Viewport from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.Viewport from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.Viewport from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/viewport.tsx

### SignIn.Step

gate

Step gates on name and accepts render={<MotionPanel />} to animate; without render it is a wrapper-free gate. Status is the one polite live region, mounted for the card's life, that speaks waits and the attempt budget. Viewport is a plain container for animated steps. Error is <div role="alert"> with { code, message }, only for the current step's last failed action. RetryAfter gets { seconds } while a rate limit holds. Restart returns to identify and drops the challenge.

Import: @slices.sh/auth/headless

```tsx
GateProps & { name: SignInStep; render?: ReactElement }

export type SignInStep =
  | "identify"
  | "reset-request"
  | "reset-code"
  | "reset"
  | "code"
  | "two-factor"
  | "locked"
  | "expired"
  | "signed-in"
```

Alternate import: SignIn.Step from @slices.sh/auth/headless/sign-in

Alternate import: SignIn.Step from @slices.sh/auth/headless/passkeys

Alternate import: SignIn.Step from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/sign-in/step.tsx
