Password input
The password field, in the flow's two dialects:
- With a
labelit is a floating label field — the same control as every other text field on the join screens, with the reveal toggle as its trailing addon. Use this wherever the surrounding fields float their labels. - Without one it is the classic lock-icon + placeholder field the login screen uses, where no field on the screen has a floating label.
It deliberately does not render a strength meter. The tucked error card
requires FieldError to be the control's adjacent
sibling, so anything the field wrapped around itself would wedge between the
two and detach the error into a floating pill. Where a password is being
chosen, compose PasswordStrengthMeter after the error line.
Examples
Login (no label)
Signing in, changing an email, confirming a destructive action — anywhere the password is being checked rather than chosen. Lock icon, placeholder, reveal toggle.
Floating label
The join-flow dialect: pass label and it becomes a
FloatingLabelInput with the eye as its
trailing addon. variant="white" is the join screens' white card on the auth
shell's grey.
Strength
Compose PasswordStrengthMeter where a password is being chosen — signup,
reset, change. Four rungs, filled to the score, because a member deciding
whether to keep typing needs to see how far they have to go: a single "weak"
says the password is bad but not that one more word would fix it.
Order matters: control, then its FieldError, then the meter — never the
meter between the two.
<Field data-invalid={invalid}>
<PasswordInput label={label} ... />
{invalid && <FieldError errors={[error]} />}
<PasswordStrengthMeter value={password} />
</Field>Invalid and disabled
The same states in both dialects. aria-invalid paints the halo and turns a
floating label destructive; the message belongs in a
FieldError, not in the meter — the meter reports
strength, never form validity.
Scoring
scorePassword lives beside the component in password-input.utils.ts and
returns 0–4, or null for an empty field. Empty is not "weak": the meter
stays silent until it has something to judge.
It is a hint, not the gate. The gate is the schema's minimum length and the
auth provider's own policy behind it. The meter exists to talk someone out of
hannah1 before they commit to it, so it punishes what actually makes a
password guessable rather than just counting characters:
- Under
PASSWORD_MIN_LENGTH(8) scores0, whatever it contains. - A leak-list password scores
0— including one wearing decoration, soPassword123!is recognised as the word it is. - Length and character variety climb the score together.
- A long passphrase is strong on length alone:
correct horse battery staplemust not score belowAa1!aa1!for lacking a digit. - Repeated runs (
aaa) and keyboard/alphabet runs (abcd,qwer) each cost a rung, whatever the length bought.
Import PASSWORD_MIN_LENGTH from the same file when writing the validation
schema, so the gate and the meter cannot drift apart.
Notes
- The meter takes the current
value— wire it to the same state orwatchthe form field, so it never depends on how the caller wired the control. - The verdict is
aria-live="polite", so a screen reader hears it change without the value being read back on every keystroke. - The rungs are
aria-hidden— the text carries the meaning. - Score
0still lights one rung, in red: an unusable password must not look like an empty field. - Set
autoComplete="new-password"when choosing and"current-password"when signing in, so password managers offer the right thing.
API
InputGroupInput's props, plus:
| Prop | Type | Description |
|---|---|---|
| label | string | Switches to the floating-label dialect. Doubles as the control's accessible name. |
| variant | InputGroup['variant'] | The surface: default grey, or 'white' for the auth shell's card. |
PasswordStrengthMeter:
| Prop | Type | Description |
|---|---|---|
| value | string | The password being judged. Empty renders a silent meter. |
| className | string | Extra classes on the meter's wrapper. |