react.form.password-field
A password input with a Show/Hide button and an optional live checklist of a password policy (GOV.UK password input).
React ^19 · TypeScript only A React component: use it from a TypeScript project with React installed. A client component ("use client").
1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 22 tests, run in TypeScript.passwordChecklist 10 · PasswordField 12
What it does
A password input with a Show/Hide button, following the GOV.UK Design System's password input, and, when you give it a password policy, a checklist of that policy's rules under the input, ticked as the person types. The rules and the check come from `auth.password-policy`, which runs in all three languages, so the checklist in the browser and the server that decides can never disagree.
"use client";
import { PasswordField } from "#fune/react.form.password-field@^1";
import { passwordPolicy } from "#fune/auth.password-policy@^1";
const policy = passwordPolicy("nist-800-63b-4-single-factor");
// Signing in
<PasswordField id="password" label="Password" error={errors.password} />
// Choosing one
<PasswordField id="new-password" name="password" label="Create a password"
policy={policy} email={account.email} personName={account.name}
value={password} onChange={setPassword} />
// and on the server: checkPassword(password, email, name, policy).valid
The functions
A group: 2 functions that work together, each in its own file, each pinned by its own tests, written for React ^19. A project can install only the ones it calls.
- passwordChecklist (policy: PasswordPolicy, check: PasswordCheck) -> PasswordChecklistItem[]
- PasswordField (props: PasswordFieldProps) -> element
The types it declares, generated into your project
/** One rule of a password policy, and whether the password meets it. */
export interface PasswordChecklistItem {
/** the failure that breaks this rule */
readonly rule: PasswordFailureCode;
/** the rule as a person reads it, such as "At least 15 characters" */
readonly text: string;
/** the password keeps this rule */
readonly met: boolean;
}
export type PasswordAutocomplete = "current-password" | "new-password";
/** A password, typed hidden, with a button to show it. */
export interface PasswordFieldProps {
/** the field's name, usually "Password" */
readonly label: ReactNode;
/** the input's id; React's useId() when left out */
readonly id?: string;
/** what the value is submitted as; the id when left out */
readonly name?: string;
/** help under the label */
readonly hint?: ReactNode;
/** what is wrong; marks the input invalid and describes it */
readonly error?: ReactNode;
/** the password, for a controlled input (with onChange) */
readonly value?: string;
/** the starting password, for an uncontrolled one */
readonly defaultValue?: string;
/** called with the new password as the person types */
readonly onChange?: (value: string) => void;
/** called when the input loses focus, to mark the field touched */
readonly onBlur?: () => void;
/** current-password to sign in, new-password to choose one; new-password when a policy is given */
readonly autoComplete?: PasswordAutocomplete;
/** show a checklist of this policy's rules under the input, ticked as they are met */
readonly policy?: PasswordPolicy;
/** the account's email, so the checklist can refuse it inside the password */
readonly email?: string;
/** the person's name, so the checklist can refuse it inside the password */
readonly personName?: string;
/** the browser refuses an empty value */
readonly required?: boolean;
/** shown but not editable, and not submitted */
readonly disabled?: boolean;
/** added to the wrapper's classes */
readonly className?: string;
}
Once installed, your code imports each one from the group's module.
passwordChecklist 10 tests
export function passwordChecklist(policy: PasswordPolicy, check: PasswordCheck): readonly PasswordChecklistItem[]
| policy | PasswordPolicy | the policy the password is checked against |
| check | PasswordCheck | checkPassword's answer for the password as typed |
| returns | PasswordChecklistItem[] | one item per rule the policy has, in checkPassword's order, each met or not |
For example
passwordChecklist(name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true, valid true, failures )→ ×5 a password meeting NIST's single-factor policy ticks every rulepasswordChecklist(name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true, valid false, failures ×1)→ ×5 too short is the first rule unmetpasswordChecklist(name nist-800-63b-4-multi-factor, min length 8, max length 128, min character classes 0, block common true, block personal true, valid false, failures ×2)→ ×5 too short and too common, both unmet
import { passwordChecklist } from "#fune/react.form.password-field@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import type { PasswordCheck, PasswordFailureCode, PasswordPolicy } from "./auth_password_policy.ts"; ← from auth.password-policy ^1.0.0 · built alongside by fune
import type { PasswordChecklistItem } from "./react_form_password_field_types.ts";
/**
* The policy's rules as a checklist, in the order checkPassword reports
* failures, each met unless the check lists its failure code. A rule the
* policy does not have (no character classes, no common-password list) is
* not listed, so the list only promises what the server will enforce.
*/
export function passwordChecklist(policy: PasswordPolicy, check: PasswordCheck): readonly PasswordChecklistItem[] {
const broken = new Set<PasswordFailureCode>(check.failures.map((failure) => failure.code));
const rules: [PasswordFailureCode, string][] = [
["too_short", `At least ${policy.minLength} ${policy.minLength === 1 ? "character" : "characters"}`],
["too_long", `No more than ${policy.maxLength} ${policy.maxLength === 1 ? "character" : "characters"}`],
];
if (policy.minCharacterClasses > 0) {
rules.push(["too_few_character_classes", `At least ${policy.minCharacterClasses} of: lower-case letters, capital letters, digits, symbols`]);
}
if (policy.blockCommon) rules.push(["too_common", "Not a commonly used password"]);
if (policy.blockPersonal) {
rules.push(["contains_email", "Does not include your email address"]);
rules.push(["contains_name", "Does not include your name"]);
}
return rules.map(([rule, text]) => ({ rule, text, met: !broken.has(rule) }));
}PasswordField throws on bad input 12 tests
export function PasswordField(props: PasswordFieldProps): JSX.Element
Its props, PasswordFieldProps. A ? marks one the caller may leave out.
| label | ReactNode | the field's name, usually "Password" |
| id? | string | the input's id; React's useId() when left out |
| name? | string | what the value is submitted as; the id when left out |
| hint? | ReactNode | help under the label |
| error? | ReactNode | what is wrong; marks the input invalid and describes it |
| value? | string | the password, for a controlled input (with onChange) |
| defaultValue? | string | the starting password, for an uncontrolled one |
| onChange? | (value: string) => void | called with the new password as the person types |
| onBlur? | () => void | called when the input loses focus, to mark the field touched |
| autoComplete? | PasswordAutocomplete | current-password to sign in, new-password to choose one; new-password when a policy is given |
| policy? | PasswordPolicy | show a checklist of this policy's rules under the input, ticked as they are met |
| email? | string | the account's email, so the checklist can refuse it inside the password |
| personName? | string | the person's name, so the checklist can refuse it inside the password |
| required? | boolean | the browser refuses an empty value |
| disabled? | boolean | shown but not editable, and not submitted |
| className? | string | added to the wrapper's classes |
| renders | JSX.Element |
For example
-
signing in: hidden, no spell check or capitals, current-password, and the Show button hidden until it can work
<PasswordField id="password" label="Password" />renders
<div class="fune-field fune-password-field"><label class="fune-label" for="password">Password</label><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-password-field__input" id="password" type="password" spellCheck="false" autoCapitalize="none" autoComplete="current-password" name="password"/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="password" aria-label="Show password" hidden="">Show</button></div></div> -
choosing a password without a policy: new-password, with a hint
<PasswordField id="new" label="Create a password" hint="Use 15 characters or more" autoComplete="new-password" />renders
<div class="fune-field fune-password-field"><label class="fune-label" for="new">Create a password</label><div class="fune-hint" id="new-hint">Use 15 characters or more</div><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-password-field__input" id="new" type="password" spellCheck="false" autoCapitalize="none" autoComplete="new-password" aria-describedby="new-hint" name="new"/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="new" aria-label="Show password" hidden="">Show</button></div></div>
import { PasswordField } from "#fune/react.form.password-field@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
"use client";
import { useEffect, useId, useRef, useState, type JSX, type ReactNode } from "react";
import { checkPassword } from "./auth_password_policy.ts"; ← from auth.password-policy ^1.0.0 · built alongside by fune
import { FormField, fieldIds } from "./react_form_form_field.ts"; ← from react.form.form-field ^1.0.0 · built alongside by fune
import { passwordChecklist } from "./react_form_password_field_password_checklist.ts"; ← passwordChecklist, another function of this group · built into the same file, even by a slim install
import type { PasswordFieldProps } from "./react_form_password_field_types.ts";
function present(node: ReactNode): boolean {
return node !== null && node !== undefined && node !== false && node !== "";
}
/**
* The GOV.UK password input: a password field with a Show/Hide button that
* controls it and a visually hidden live status that says which it is. The
* button stays hidden until the component has mounted, as GOV.UK's does
* until its script runs, so a page without JavaScript shows no dead button.
* The password goes back to hidden when its form is submitted or the page is
* restored from the back-forward cache. With a policy, a checklist of its
* rules follows the input and describes it, ticked as the password meets them.
*/
export function PasswordField(props: PasswordFieldProps): JSX.Element {
const auto = useId();
const { label, hint, error, value, defaultValue, onChange, onBlur, policy, email, personName, required, disabled, className } = props;
const [typed, setTyped] = useState(defaultValue ?? "");
const [shown, setShown] = useState(false);
const [status, setStatus] = useState("");
const [ready, setReady] = useState(false);
const input = useRef<HTMLInputElement>(null);
useEffect(() => {
setReady(true);
const hide = () => setShown(false);
const restored = (event: PageTransitionEvent) => {
if (event.persisted) setShown(false);
};
const form = input.current?.form ?? null;
form?.addEventListener("submit", hide);
window.addEventListener("pageshow", restored);
return () => {
form?.removeEventListener("submit", hide);
window.removeEventListener("pageshow", restored);
};
}, []);
const id = props.id ?? auto;
const ids = fieldIds(id, present(hint), present(error));
const checklistId = `${id}-checklist`;
const items = policy ? passwordChecklist(policy, checkPassword(value ?? typed, email ?? null, personName ?? null, policy)) : null;
const describedBy = items ? [ids.describedBy, checklistId].filter(Boolean).join(" ") : ids.describedBy;
const wrapper = ["fune-password-field", className || null].filter(Boolean).join(" ");
return (
<FormField id={id} label={label} hint={hint} error={error} className={wrapper}>
<div className="fune-input-wrapper fune-password-field__wrapper">
<input
ref={input}
className={ids.error ? "fune-input fune-input--error fune-password-field__input" : "fune-input fune-password-field__input"}
id={ids.control}
name={props.name ?? id}
type={shown ? "text" : "password"}
value={value}
defaultValue={defaultValue}
spellCheck={false}
autoCapitalize="none"
autoComplete={props.autoComplete ?? (policy ? "new-password" : "current-password")}
required={required}
disabled={disabled}
aria-describedby={describedBy ?? undefined}
aria-invalid={ids.error ? true : undefined}
onChange={(event) => {
setTyped(event.target.value);
onChange?.(event.target.value);
}}
onBlur={onBlur ? () => onBlur() : undefined}
/>
<div className="fune-password-field__status fune-visually-hidden" aria-live="polite">
{status}
</div>
<button
className="fune-password-field__toggle"
type="button"
aria-controls={ids.control}
aria-label={shown ? "Hide password" : "Show password"}
hidden={!ready}
disabled={disabled}
onClick={() => {
setShown(!shown);
setStatus(shown ? "Your password is hidden" : "Your password is visible");
}}
>
{shown ? "Hide" : "Show"}
</button>
</div>
{items ? (
<ul className="fune-password-checklist" id={checklistId}>
{items.map((item) => (
<li
key={item.rule}
className={item.met ? "fune-password-checklist__item fune-password-checklist__item--met" : "fune-password-checklist__item fune-password-checklist__item--unmet"}
>
<span className="fune-visually-hidden">{item.met ? "Done:" : "Not yet:"}</span> {item.text}
</li>
))}
</ul>
) : null}
</FormField>
);
}Install
npm install react react-dom
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project) with React ^19 installed (Functional Weave does not ship it, and fune build stops with the npm install line if it is missing), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add react.form.password-field
That builds the whole group. To build only what you call, and whatever it uses inside the group:
fune add react.form.password-field --only passwordChecklist
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./react.form.password-field-1.0.0-typescript.fune, or fetch it from a terminal with fune pull react.form.password-field@1.0.0:typescript.
It is React ^19 · TypeScript only, so there is no package for Python or Rust. The full package is one file too: react.form.password-field-1.0.0.fune, 41,188 bytes, sha256 817115583d7b157257ece3251b6fec053beacb71e7e990f0f0abcf8a47c544c5.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before react.form.password-field.passwordChecklist
// fune: before react.form.password-field.PasswordField
after — your function gets the result and the arguments, and returns the final result.
// fune: after react.form.password-field.passwordChecklist
// fune: after react.form.password-field.PasswordField
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace auth.password-policy in react.form.password-field
// fune: replace react.form.form-field in react.form.password-field
step — your function runs at a numbered point inside a function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show react.form.password-field --steps.
// fune: step react.form.password-field.<fn> after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript alone, with the React in tooling/react, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json. A component’s tests are render tests: the props go in, and the HTML renderToStaticMarkup makes of them has to match exactly; a React warning during the render fails the test.
passwordChecklist 10 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a password meeting NIST's single-factor policy ticks every rule | name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true, valid true, failures | → | ×5 |
| too short is the first rule unmet | name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true, valid false, failures ×1 | → | ×5 |
| too short and too common, both unmet | name nist-800-63b-4-multi-factor, min length 8, max length 128, min character classes 0, block common true, block personal true, valid false, failures ×2 | → | ×5 |
| the email and the name are separate rules | name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true, valid false, failures ×2 | → | ×5 |
| only the name is in the password | name nist-800-63b-4-multi-factor, min length 8, max length 128, min character classes 0, block common true, block personal true, valid false, failures ×1 | → | ×5 |
| over the maximum length | name nist-800-63b-4-multi-factor, min length 8, max length 128, min character classes 0, block common true, block personal true, valid false, failures ×1 | → | ×5 |
| a composition policy lists its character classes, after the lengths | name composition-12-3, min length 12, max length 128, min character classes 3, block common true, block personal true, valid false, failures ×2 | → | ×6 |
| a policy without a blocklist or personal words lists only the lengths, a minimum of 1 in the singular | name pin, min length 1, max length 64, min character classes 0, block common false, block personal false, valid true, failures | → | ×2 |
| one character class, no blocklist, personal words refused | name personal-only, min length 10, max length 64, min character classes 1, block common false, block personal true, valid false, failures ×1 | → | ×5 |
| a failure the policy has no rule for is not listed | name common-only, min length 8, max length 8, min character classes 0, block common true, block personal false, valid false, failures ×1 | → | ×3 |
PasswordField 12 tests
-
signing in: hidden, no spell check or capitals, current-password, and the Show button hidden until it can work
<PasswordField id="password" label="Password" />renders
<div class="fune-field fune-password-field"><label class="fune-label" for="password">Password</label><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-password-field__input" id="password" type="password" spellCheck="false" autoCapitalize="none" autoComplete="current-password" name="password"/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="password" aria-label="Show password" hidden="">Show</button></div></div> -
choosing a password without a policy: new-password, with a hint
<PasswordField id="new" label="Create a password" hint="Use 15 characters or more" autoComplete="new-password" />renders
<div class="fune-field fune-password-field"><label class="fune-label" for="new">Create a password</label><div class="fune-hint" id="new-hint">Use 15 characters or more</div><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-password-field__input" id="new" type="password" spellCheck="false" autoCapitalize="none" autoComplete="new-password" aria-describedby="new-hint" name="new"/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="new" aria-label="Show password" hidden="">Show</button></div></div> -
an error marks the input invalid and describes it
<PasswordField id="password" label="Password" error="Enter your password" />renders
<div class="fune-field fune-field--error fune-password-field"><label class="fune-label" for="password">Password</label><p class="fune-error-message" id="password-error"><span class="fune-visually-hidden">Error:</span> Enter your password</p><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-input--error fune-password-field__input" id="password" type="password" spellCheck="false" autoCapitalize="none" autoComplete="current-password" aria-describedby="password-error" aria-invalid="true" name="password"/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="password" aria-label="Show password" hidden="">Show</button></div></div> -
a name of its own; required and disabled, and the button disabled with it
<PasswordField id="pw" name="password" label="Password" required disabled />renders
<div class="fune-field fune-password-field"><label class="fune-label" for="pw">Password</label><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-password-field__input" id="pw" type="password" spellCheck="false" autoCapitalize="none" autoComplete="current-password" required="" disabled="" name="password"/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="pw" aria-label="Show password" hidden="" disabled="">Show</button></div></div> -
a controlled value is written last, escaped
<PasswordField id="pw" label="Password" value="a&b\"c" />renders
<div class="fune-field fune-password-field"><label class="fune-label" for="pw">Password</label><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-password-field__input" id="pw" type="password" spellCheck="false" autoCapitalize="none" autoComplete="current-password" name="pw" value="a&b"c"/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="pw" aria-label="Show password" hidden="">Show</button></div></div> -
a policy adds a checklist that describes the input, and makes it new-password; empty is only too short
<PasswordField id="pw" label="Create a password" policy={{"name":"nist-800-63b-4-multi-factor","minLength":8,"maxLength":128,"minCharacterClasses":0,"blockCommon":true,"blockPersonal":true}} defaultValue="" />renders
<div class="fune-field fune-password-field"><label class="fune-label" for="pw">Create a password</label><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-password-field__input" id="pw" type="password" spellCheck="false" autoCapitalize="none" autoComplete="new-password" aria-describedby="pw-checklist" name="pw" value=""/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="pw" aria-label="Show password" hidden="">Show</button></div><ul class="fune-password-checklist" id="pw-checklist"><li class="fune-password-checklist__item fune-password-checklist__item--unmet"><span class="fune-visually-hidden">Not yet:</span> At least 8 characters</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> No more than 128 characters</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> Not a commonly used password</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> Does not include your email address</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> Does not include your name</li></ul></div> -
the checklist follows the typed password: the email's local part and the name are both in it
<PasswordField id="pw" label="Create a password" hint="Use a long passphrase" policy={{"name":"nist-800-63b-4-single-factor","minLength":15,"maxLength":128,"minCharacterClasses":0,"blockCommon":true,"blockPersonal":true}} email="ada.lovelace@example.com" personName="Ada Lovelace" value="lovelacebatteryhorse" />renders
<div class="fune-field fune-password-field"><label class="fune-label" for="pw">Create a password</label><div class="fune-hint" id="pw-hint">Use a long passphrase</div><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-password-field__input" id="pw" type="password" spellCheck="false" autoCapitalize="none" autoComplete="new-password" aria-describedby="pw-hint pw-checklist" name="pw" value="lovelacebatteryhorse"/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="pw" aria-label="Show password" hidden="">Show</button></div><ul class="fune-password-checklist" id="pw-checklist"><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> At least 15 characters</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> No more than 128 characters</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> Not a commonly used password</li><li class="fune-password-checklist__item fune-password-checklist__item--unmet"><span class="fune-visually-hidden">Not yet:</span> Does not include your email address</li><li class="fune-password-checklist__item fune-password-checklist__item--unmet"><span class="fune-visually-hidden">Not yet:</span> Does not include your name</li></ul></div> -
a composition policy: twelve characters, but two classes and on the common list
<PasswordField id="pw" label="New password" policy={{"name":"composition-12-3","minLength":12,"maxLength":128,"minCharacterClasses":3,"blockCommon":true,"blockPersonal":true}} value="q1w2e3r4t5y6" />renders
<div class="fune-field fune-password-field"><label class="fune-label" for="pw">New password</label><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-password-field__input" id="pw" type="password" spellCheck="false" autoCapitalize="none" autoComplete="new-password" aria-describedby="pw-checklist" name="pw" value="q1w2e3r4t5y6"/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="pw" aria-label="Show password" hidden="">Show</button></div><ul class="fune-password-checklist" id="pw-checklist"><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> At least 12 characters</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> No more than 128 characters</li><li class="fune-password-checklist__item fune-password-checklist__item--unmet"><span class="fune-visually-hidden">Not yet:</span> At least 3 of: lower-case letters, capital letters, digits, symbols</li><li class="fune-password-checklist__item fune-password-checklist__item--unmet"><span class="fune-visually-hidden">Not yet:</span> Not a commonly used password</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> Does not include your email address</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> Does not include your name</li></ul></div> -
an error, a policy and a class together: described by the error then the checklist
<PasswordField id="pw" label="Password" error="Choose a longer password" policy={{"name":"nist-800-63b-4-multi-factor","minLength":8,"maxLength":128,"minCharacterClasses":0,"blockCommon":true,"blockPersonal":true}} value="short" className="wide" autoComplete="new-password" />renders
<div class="fune-field fune-field--error fune-password-field wide"><label class="fune-label" for="pw">Password</label><p class="fune-error-message" id="pw-error"><span class="fune-visually-hidden">Error:</span> Choose a longer password</p><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-input--error fune-password-field__input" id="pw" type="password" spellCheck="false" autoCapitalize="none" autoComplete="new-password" aria-describedby="pw-error pw-checklist" aria-invalid="true" name="pw" value="short"/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="pw" aria-label="Show password" hidden="">Show</button></div><ul class="fune-password-checklist" id="pw-checklist"><li class="fune-password-checklist__item fune-password-checklist__item--unmet"><span class="fune-visually-hidden">Not yet:</span> At least 8 characters</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> No more than 128 characters</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> Not a commonly used password</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> Does not include your email address</li><li class="fune-password-checklist__item fune-password-checklist__item--met"><span class="fune-visually-hidden">Done:</span> Does not include your name</li></ul></div> -
the label is escaped
<PasswordField id="p" label="Password <b>& PIN</b>" />renders
<div class="fune-field fune-password-field"><label class="fune-label" for="p">Password <b>& PIN</b></label><div class="fune-input-wrapper fune-password-field__wrapper"><input class="fune-input fune-password-field__input" id="p" type="password" spellCheck="false" autoCapitalize="none" autoComplete="current-password" name="p"/><div class="fune-password-field__status fune-visually-hidden" aria-live="polite"></div><button class="fune-password-field__toggle" type="button" aria-controls="p" aria-label="Show password" hidden="">Show</button></div></div>
Show the other 2 tests
-
an id with a space is refused
<PasswordField id="my password" label="Password" />error: a field id cannot contain spaces
-
a policy with a minimum of 0 is refused by checkPassword
<PasswordField id="pw" label="Password" policy={{"name":"broken","minLength":0,"maxLength":10,"minCharacterClasses":0,"blockCommon":false,"blockPersonal":false}} />error: policy minLength must be a whole number of at least 1
More from the author
## What it renders
- The input is `type="password"` with `spellCheck={false}` and `autoCapitalize="none"`, so a phone neither corrects nor capitalises it, and `autoComplete` `current-password` (signing in) or `new-password` (choosing one, the default when a `policy` is given), which is what lets a password manager fill or generate it. - The **Show** button is `type="button"` (it never submits the form), has `aria-controls` naming the input and an `aria-label` of "Show password" or "Hide password", and reads "Show" or "Hide". It is rendered `hidden` until the component has mounted, as GOV.UK hides it until its script runs, so a page without JavaScript never shows a button that does nothing. It is disabled with the field. - A visually hidden `aria-live="polite"` status beside the input says "Your password is visible" or "Your password is hidden" after each press. It starts empty, so nothing is announced on page load. - The password is hidden again when its form is submitted, and when the page comes back from the back-forward cache, so it is never left on screen. - With a **policy**, a list follows the input, one item per rule the policy has: "At least 15 characters", "No more than 128 characters", "At least 3 of: lower-case letters, capital letters, digits, symbols" (only when the policy counts classes), "Not a commonly used password" (only with the blocklist), "Does not include your email address" and "Does not include your name" (only when personal words are refused). Each starts with a visually hidden "Done:" or "Not yet:", so the state is heard as well as seen. The list is in the input's `aria-describedby` (after the hint and the error) but is not a live region: announcing it on every key would drown the person typing. **email** and **personName** feed the last two rules; the input's submitted name is `name`, so the person's name has its own prop. An empty password shows only the minimum length as not yet met: the other rules are not broken by nothing.
`passwordChecklist(policy, check)` is the list as data, for a page that draws its own, and is what the component renders. It only turns `checkPassword`'s failure codes into rules; a failure the policy has no rule for is not listed.
`onChange` is called with the password, not the event, and `onBlur` with nothing when the input loses focus, to mark the field touched.
Ids: the input is `id` (React's `useId()` when left out), the hint `<id>-hint`, the error `<id>-error`, the checklist `<id>-checklist`. It is a client component (state for the toggle and the checklist), so its file starts with `"use client"`. Errors are those of `react.form.form-field` (an id with a space) and `checkPassword` (a nonsense policy).
Classes: those of `react.form.form-field`, plus `fune-password-field` on the wrapper, `fune-input` (and `fune-input--error`), `fune-input-wrapper`, `fune-password-field__wrapper`, `fune-password-field__input`, `fune-password-field__status`, `fune-password-field__toggle`, `fune-password-checklist`, `fune-password-checklist__item` (and `fune-password-checklist__item--met`, `fune-password-checklist__item--unmet`).
Sources: GOV.UK Design System, "Password input" https://design-system.service.gov.uk/components/password-input/ and its script, for the button and status wording https://github.com/alphagov/govuk-frontend/tree/main/packages/govuk-frontend/src/govuk/components/password-input; HTML autofill tokens `current-password` and `new-password` https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill; NIST SP 800-63B-4 section 3.1.1.2 (via `auth.password-policy`) https://pages.nist.gov/800-63-4/sp800-63b.html.
Files
| Path | Bytes |
|---|---|
| README.md | 4,708 |
| impl/typescript/password_checklist.ts | 1,499 |
| impl/typescript/password_field.tsx | 4,647 |
| vectors.json | 21,282 |