# Create password

> A password step with a chameleon: the stronger the password, the better Hide blends in, until only his eye is left.

The password rules box is the most ignored part of every sign-up. This one hands it to Hide, an illustrated chameleon sitting on a branch on the lit stage, watching the caret as you type. Each rule the password meets, his tongue shoots out to its circle and leaves a check there; lose a rule and he takes the check back and swallows it. With every check a front of colour change runs a little further from his tail to his snout, leaving only his shading in the stage’s own colour, so with every rule met he’s gone but for an eye blinking out of the scales. Press Show password and his cover is blown: the colour floods back, his eye squeezes shut and his cheek flushes. Wipe the field after he’s hidden and he jumps. Give it checkBreaches and a password that turns up in known breaches turns him red and says how often. Under the act it is a proper password step: a real new-password field password managers fill and save, rules you set, Continue that stays off until they’re met, and onSubmit for your endpoint.

## Install

```bash
npx shadcn@latest add https://hairlineui.com/r/create-password.json
```

Needs React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

- npm: none
- Hairline UI components it needs (the CLI installs them too): `password-input`, `icon-morph`, `text-morph`, `status-button`
- Docs and live preview: https://hairlineui.com/ui/create-password
- Registry item (source): https://hairlineui.com/r/create-password.json

## Usage

```tsx
import { CreatePassword } from "@/components/create-password";

<CreatePassword
  username={email}
  onSubmit={(password) => fetch("/api/password", { method: "POST", body: JSON.stringify({ password }) })}
/>
```

## With a breach check

Have I Been Pwned’s range API needs no key and never sees the password: send the first five characters of its SHA-1 and look for the rest in the answer.

```tsx
"use client";
import { CreatePassword } from "@/components/create-password";

async function checkBreaches(password: string, signal: AbortSignal) {
  const bytes = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(password));
  const hash = [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
  const res = await fetch(`https://api.pwnedpasswords.com/range/${hash.slice(0, 5)}`, { signal });
  const line = (await res.text()).split("\n").find((l) => l.startsWith(hash.slice(5)));
  return line ? Number(line.split(":")[1]) : 0;
}

export function PasswordStep({ email }: { email: string }) {
  return (
    <CreatePassword
      username={email}
      checkBreaches={checkBreaches}
      onSubmit={async (password) => {
        const res = await fetch("/api/sign-up/password", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ email, password }),
        });
        if (!res.ok) throw new Error((await res.json()).message ?? "Couldn’t save that. Try again.");
      }}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onSubmit` | `(password: string) => Promise<unknown>` | – | Called with the password once every rule is met. Keep it; throw an Error to show its message. |
| `checkBreaches` | `(password: string, signal: AbortSignal) => Promise<number>` | – | How many known breaches the password is in (0 if none). Asked 450ms after typing stops once the rules are met, and again on Continue if needed; a newer password aborts the older ask. If it fails, Continue still works. |
| `rules` | `PasswordRule[]` | `PASSWORD_RULES` | { id, label, test(password) }. Defaults: 8+ characters, a number, a capital letter, a symbol. |
| `username` | `string` | – | The account’s email or username, in a hidden field so password managers save the pair. |
| `onValueChange` | `(password: string) => void` | – | Every change to the password. |
| `title / description / submitLabel` | `string` | `"Create a password" / "The stronger it is, the harder Hide is to find." / "Continue"` | The heading, the line under it and the button. |
| `name` | `string` | `"Hide"` | What the chameleon is called in his lines. |

## States

- **spotted**: Hide sits on his branch in full colour, his pupil following the caret (or the pointer between keystrokes); the line above reads “Hide is easy to spot”. He breathes and blinks now and then.
- **catch**: A rule met: he glances at its circle and his tongue shoots out (150ms), touches it, and the check pops in and draws itself as the tongue comes back (210ms). Several at once go one after another, faster.
- **blend**: Each check moves a front of colour change further from tail to snout (about 600ms): behind it only his shading is left, in the stage’s colour, with a brighter band at the front as it passes.
- **gone**: Every rule met: only his eye is left in the scales and the line asks “Where did Hide go?”. Continue lights up.
- **take back**: A rule lost: his tongue fetches the check, he swallows it with a little bob and the colour comes back a step.
- **exposed**: Show password: the colour floods back at once, his eye squeezes shut and his cheek flushes; hiding it lets him blend in again.
- **startle**: Wiping the field once two or more rules were met: the checks fall away, he jumps with a tiny pupil and says “Hey! I was hiding.”
- **breached**: checkBreaches returns more than 0: he turns red, the field says how many breaches the password was seen in, and Continue stays off.
- **saved**: Continue → Checking → Saving → Password set with a drawn check, and he winks; if onSubmit throws, the button shakes into Try again and the message shows under the field.

## What it handles

A password step is usually a field, a grey list of rules and a meter that turns from red to green. This block makes each rule a moment, checks the password against known breaches, and keeps the field one password managers understand.

- **Rules that react**: Each rule met gets its check from Hide’s tongue; lose one and he takes it back.
- **Strength you can see**: He blends further into the stage with every rule, until only his eye is left.
- **Breach check**: checkBreaches runs once the rules are met, after a pause in the typing; a breached password turns him red and blocks Continue.
- **Your rules**: Pass any rules as { id, label, test }; the defaults are length, a number, a capital and a symbol.
- **Password managers**: A new-password field with the account’s username beside it, so the pair is offered and saved.
- **Submit with states**: Continue → Checking → Saving → Password set, or the server’s message under the field.

## Replaces

a password strength meter, a rules checklist, a show-password toggle, a breach check, a submit button with states

## Where it fits

- **Sign-up**: The password step after the email, with rules people actually read.
- **Password reset**: The new-password screen at the end of a reset link.
- **Account security**: Change password in settings, with a breach check before it’s saved.
- **Invites**: Setting a password when accepting a team invite.

## Notes

- The field is a real new-password input labelled Password; the eye is a toggle button with aria-pressed. Each rule is a list item that says done or not yet, and every check placed or taken back is announced from a status region.
- Hide is decoration (aria-hidden). He is an illustration shipped inside hide-art.ts as two WebP data URIs (43KB of images, 59KB as text), so the install is complete with nothing to put in public/; his pupil, tongue, blush, colour change and hop are drawn in code over it.
- One frame loop outside React writes his styles and sleeps when he’s still; his breathing is a compositor animation. The tongue is drawn in an overlay over the block and costs no React renders.
- The stage is the lit --primary surface engraved with fine scales; he blends into whatever colour your theme gives it, light or dark. Tongue, pupil, blush and check colours are --hide-* variables you can override. No drop shadows, glow or blur.
- With reduced motion there is no tongue, hop or breathing: checks appear in place and his colour changes in place.
- Built from Password input, Status button and Text morph, which the CLI installs with it. No animation library.

License: MIT (https://hairlineui.com/license#components)
