Password Input
Reveal toggle, Caps Lock warning, a strength meter that shows the rules — and why blocking paste makes accounts less secure, not more.
Also called Secret Field, Passphrase Input — in this system all of them are Password Input.
Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.
At least 12 characters, mixed case, and a number or symbol.
- At least 12 characters
- Upper and lower case
- A number or symbol
Sign in vs. sign up
Two different fields wearing the same mask. Sign-in wants current-password and no meter; sign-up wants new-password, the rules, and no confirm field if a reveal toggle exists.
Caps Lock warning
The single highest-value affordance on a sign-in form. The user cannot see what they typed, so an all-caps password is otherwise an unexplainable failure.
The meter must show the rules
A bar labelled "Weak" is a rejection. The same bar with three live checks is a set of instructions the user can act on without guessing.
A reveal toggle replaces the confirm field
Confirm exists because the user cannot see what they typed. Give them a reveal control and the second field is a second chance to make the same typo twice.
Interaction states
Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.
Every part, every measurement, and the reason it is that number.
At least 12 characters.
A masked field with a reveal toggle inside it, and a strength meter that appears only once the user has typed something.
- Mask characterBrowser default (•)
Never a custom character. The native mask is what password managers and screen readers detect, and replacing it breaks both.
- Reveal toggle28px, inside the field
Inside the border so it reads as part of the control. Its accessible name changes with the state — "Show password" / "Hide password" — and it is a toggle button, not a checkbox.
- Reveal timeoutOptional, 8–15s
Re-masking after a delay protects a screen left unattended. Only defensible on shared devices; on a personal one it is an interruption.
- Caps Lock warningWarning tone, on focus
Detected from the keyboard event’s modifier state, shown only while the field has focus. Shouting about Caps Lock on an unfocused field is noise.
- Strength meter4px bar + rule list
Appears after the first character, never before. An empty field showing "Weak" is an accusation.
- Rule list12px, live per rule
Each rule ticks as it is met. This is the part that teaches; the bar alone is a verdict with no appeal.
Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.
Color
| Token | Value | Used for |
|---|---|---|
| --ds-surface-inset | — | Field fill |
| --ds-border-interactive | — | Idle border |
| --ds-accent | — | Focus border |
| --ds-warning-border | — | Caps Lock border |
| --ds-warning-text | — | Caps Lock message |
| --ds-danger | — | Weak strength |
| --ds-success | — | Strong strength and met rules |
| --ds-fg-muted | — | Reveal glyph and unmet rules |
Spacing
| Token | Value | Used for |
|---|---|---|
| padding-right | Room for the reveal toggle |
Radius
| Token | Value | Used for |
|---|---|---|
| --radius-md | Field corners |
Typography
| Token | Value | Used for |
|---|---|---|
| --text-caption | Rule list |
Motion
| Token | Value | Used for |
|---|---|---|
| --duration-fast | Meter fill transition |
Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.
| Size | Height | Padding | Radius | Type | Min width | Touch target | When to use |
|---|---|---|---|---|---|---|---|
| Small | 32px | 0 36px 0 10px | — | 13px | — | — | Re-authentication inside a dialog. |
| Medium | 36px | 0 40px 0 12px | — | 15px | — | — | The default. |
| Large | 44px | 0 44px 0 14px | — | 16px | — | — | Sign-in and sign-up pages, where the field is the page. |
| Reveal toggle | 28px | — | — | — | 28px | 44px on coarse pointers | Inside the field, right-aligned, 6px from the edge. |
| Strength bar | 4px | — | full | — | — | — | Full field width, directly beneath it, with the label beside the value. |
✗ onPaste={(e) => e.preventDefault()}
✓ nothing — paste is the defaultCaps Lock is on.
Must contain a symbol.
Maximum 16 characters.
Too long.
Not a checklist to run at the end. These are the requirements the component was built from.
Contrast
- The mask dots owe 4.5:1. They are how a user counts what they typed, and browsers often render them lighter than the input colour.
- The strength meter must not rely on colour alone — the label ("Weak", "Strong") carries the same information for the 8% who cannot distinguish red from green.
- Met and unmet rules differ by icon as well as colour, for the same reason.
- The reveal glyph at 16px owes 4.5:1 against the field fill.
Keyboard
| Tab | Reaches the field, then the reveal toggle. The toggle is a real button and must be reachable. |
| Space / Enter | Toggles reveal. Focus stays on the toggle; it must not jump back to the field. |
| ⌘ / Ctrl + V | Pastes. Never intercepted. |
| Enter | Submits the form from the field, as in any single-line input. |
Screen readers
- Announce the requirements before the user types, via aria-describedby. Discovering them through failure is the worst possible order.
- The reveal toggle must announce its state: "Show password, toggle button, not pressed".
- Caps Lock must be announced, not just coloured. It is the one warning where the affected user is least able to see what went wrong.
- WCAG 2.2’s Accessible Authentication criterion means no cognitive test may be required to sign in — paste, autofill and a reveal toggle are how you satisfy it.
Focus & touch
- Toggling reveal must not move focus or reset the caret. The naive implementation swaps the input type and the caret jumps to the end — preserve selectionStart and selectionEnd across the swap.
- The reveal toggle needs a 44px target, which usually means growing the field to 44px rather than shrinking the glyph. Set autocomplete correctly so the platform keychain offers to fill — on mobile that is how most people sign in, and a wrong token silently removes the option.
| Attribute | Applied to | Notes |
|---|---|---|
| autocomplete | The field | current-password or new-password. This is a functional attribute, and 1.3.5 requires it. |
| aria-pressed | The reveal toggle | It is a toggle button. Its label should also change — "Show password" / "Hide password". |
| aria-describedby | The field | Points at the requirements list, so the rules are read before the user types rather than after they fail. |
| role="status" | The Caps Lock warning | Polite. It must be announced, not merely coloured, or the user who most needs it never learns. |
| aria-live="polite" | The strength label | Debounced to roughly every 500ms. Announcing on every keystroke is unusable. |
| aria-invalid | The field | On a failed sign-in, set on the form rather than the field — you do not know which of the two was wrong, and guessing helps an attacker. |
Example usage
1import { Field, PasswordInput } from '@/ui/Input'23// Sign in: the password already exists, so no meter and no rules.4<Field label="Password">5 <PasswordInput autoComplete="current-password" name="password" />6</Field>78// Sign up: new-password lets the manager offer to generate one.9<Field10 label="New password"11 description="At least 12 characters, mixed case, and a number or symbol."12>13 <PasswordInput autoComplete="new-password" name="new-password" />14</Field>1516// Caps Lock: read the modifier state from the event, only while focused.17const [caps, setCaps] = React.useState(false)18<input19 onKeyUp={(e) => setCaps(e.getModifierState('CapsLock'))}20 onKeyDown={(e) => setCaps(e.getModifierState('CapsLock'))}21 onBlur={() => setCaps(false)}22/>2324// Preserve the caret across the type swap. The naive version sends the25// cursor to the end every time the user peeks.26function toggleReveal(el: HTMLInputElement) {27 const { selectionStart, selectionEnd } = el28 setRevealed((r) => !r)29 requestAnimationFrame(() => el.setSelectionRange(selectionStart, selectionEnd))30}Framework-free HTML
<div class="ds-field">
<label for="pw">New password</label>
<!-- Read BEFORE typing, not discovered through failure. -->
<ul id="pw-rules">
<li>At least 12 characters</li>
<li>Upper and lower case</li>
<li>A number or symbol</li>
</ul>
<div class="ds-password">
<input
id="pw"
type="password"
name="new-password"
autocomplete="new-password"
aria-describedby="pw-rules pw-strength"
/>
<!-- No maxlength. No onpaste handler. Both are anti-features. -->
<button type="button" aria-label="Show password" aria-pressed="false">
<svg aria-hidden="true">…</svg>
</button>
</div>
<p id="pw-strength" role="status" aria-live="polite">Strength: Good</p>
<p id="pw-caps" role="status" aria-live="polite" hidden>Caps Lock is on.</p>
</div>CSS
.ds-password { position: relative; }
.ds-password input {
inline-size: 100%;
block-size: 36px;
padding-inline: 12px 40px; /* room for the reveal toggle */
border: 1px solid var(--ds-border-interactive);
border-radius: var(--radius-md);
background: var(--ds-surface-inset);
/* Never set -webkit-text-security or a custom mask character: it is what
password managers and screen readers detect. */
}
.ds-password button {
position: absolute;
inset-inline-end: 6px;
inset-block-start: 50%;
translate: 0 -50%;
inline-size: 28px;
block-size: 28px;
color: var(--ds-fg-muted); /* 4.5:1 — it is a control */
}
.ds-password input[aria-invalid='true'] { border-color: var(--ds-danger-border); }
.ds-password[data-caps='true'] input { border-color: var(--ds-warning-border); }
/* Colour is never the only signal: the label says "Weak" or "Strong" too. */
.ds-strength__bar { block-size: 4px; border-radius: 999px; }
.ds-strength__bar[data-level='1'] { background: var(--ds-danger); }
.ds-strength__bar[data-level='4'] { background: var(--ds-success); }
@media (pointer: coarse) {
.ds-password input { block-size: 44px; padding-inline-end: 48px; }
.ds-password button { inline-size: 44px; block-size: 44px; }
}Component API
PasswordInput
| Prop | Type | Default | Description |
|---|---|---|---|
| autoComplete* | 'current-password' | 'new-password' | — | Functional, not optional. The wrong value breaks password managers in a way users blame on themselves. |
| size | 'sm' | 'md' | 'lg' | 'md' | The reveal toggle keeps a touch-sized target at every size. |
| status | 'default' | 'error' | 'success' | 'warning' | 'default' | Warning is the Caps Lock state; error is a failed submission. |
| revealTimeout | number | — | Milliseconds before re-masking. Only worth it on shared devices; elsewhere it is an interruption. |
| disabled | boolean | false | Removes the field from the tab order. |
Professional tips
- Do not enforce a maximum length. A cap signals the password is not being hashed, and it breaks the long passphrases managers generate.
- Check against a breached-password list rather than adding composition rules. "Summer2024!" satisfies every rule most products ship and is in every wordlist.
- On a failed sign-in, never say which field was wrong. "Email or password is incorrect" is the correct message, and it is also the kind one.
- Offer the reveal toggle on sign-in too, not only sign-up. Typos happen more often on the field where there is no confirm.
- Keep the requirements visible after submission fails. Hiding them at the moment of failure is the most common version of losing the instructions.
Performance
- Debounce strength calculation by about 150ms. A real strength estimator like zxcvbn is not free on every keystroke.
- Load the strength library lazily, only on the sign-up route. It is a few hundred kilobytes that the sign-in page has no use for.
- Never send the password anywhere for strength checking. Breach checks use k-anonymity — send the first five characters of the SHA-1 hash and nothing else.
Common mistakes
- Blocking paste, which pushes users off password managers and onto weaker passwords.
- The wrong autocomplete token, so the manager fills nothing or overwrites a good entry.
- A maximum length, which suggests the password is being stored rather than hashed.
- A strength bar with no rules, which rejects without instructing.
- The caret jumping to the end when the user toggles reveal.
- Enforcing composition rules at sign-in, where the password already exists.
- Custom mask characters, which break password-manager detection and screen-reader announcements.
- A Caps Lock warning that is coloured but never announced.
Real-world recommendations
- NIST guidance has recommended against composition rules and forced rotation for years. Length and a breach check outperform every symbol requirement ever shipped.
- The confirm field is a hangover from before reveal toggles existed. Products that removed it and kept the toggle report fewer failed sign-ups, not more.
- Password managers now fill the overwhelming majority of credentials. Every design decision here should be checked against "does this still work when a manager fills it?".
- Passkeys are replacing this component. Where you support them, offer the passkey first and keep the password field as the fallback — not the other way round.