Functional Weave
Code in Rust

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.1 · 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.

A React capability: React ^19 · TypeScript only. This capability has no Rust implementation, so it is shown in TypeScript. A Rust project cannot use it: fune build stops and names where it was required. Your choice of Rust is kept for every other page.

  1. passwordChecklist (policy: PasswordPolicy, check: PasswordCheck) -> PasswordChecklistItem[]
  2. 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[]
policyPasswordPolicythe policy the password is checked against
checkPasswordCheckcheckPassword's answer for the password as typed
returnsPasswordChecklistItem[]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 rule
  • 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 false, failures ×1) → ×5 too short is the first rule unmet
  • passwordChecklist(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";
impl/typescript/password_checklist.ts · 25 lines · open · raw

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.

labelReactNodethe field's name, usually "Password"
id?stringthe input's id; React's useId() when left out
name?stringwhat the value is submitted as; the id when left out
hint?ReactNodehelp under the label
error?ReactNodewhat is wrong; marks the input invalid and describes it
value?stringthe password, for a controlled input (with onChange)
defaultValue?stringthe starting password, for an uncontrolled one
onChange?(value: string) => voidcalled with the new password as the person types
onBlur?() => voidcalled when the input loses focus, to mark the field touched
autoComplete?PasswordAutocompletecurrent-password to sign in, new-password to choose one; new-password when a policy is given
policy?PasswordPolicyshow a checklist of this policy's rules under the input, ticked as they are met
email?stringthe account's email, so the checklist can refuse it inside the password
personName?stringthe person's name, so the checklist can refuse it inside the password
required?booleanthe browser refuses an empty value
disabled?booleanshown but not editable, and not submitted
className?stringadded to the wrapper's classes
rendersJSX.Element

For example

  1. 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>
  2. 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";
impl/typescript/password_field.tsx · 109 lines · open · raw

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
Download for TypeScript react.form.password-field-1.0.1-typescript.fune · 42,790 bytes sha256 2b58da4b32060ef0b345cdb4f47a806927ef27fe9a26f27ea029b236f4c0ee37

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./react.form.password-field-1.0.1-typescript.fune, or fetch it from a terminal with fune pull react.form.password-field@1.0.1: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.1.fune, 42,762 bytes, sha256 6ae4641b8c498ce5779786c7735dc6e3be0777ec7ed9e2989c543384eecfc942.

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

CaseArgumentsExpected
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

  1. 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>
  2. 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>
  3. 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>
  4. 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>
  5. 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&amp;b&quot;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>
  6. 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>
  7. 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>
  8. 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>
  9. 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>
  10. 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 &lt;b&gt;&amp; PIN&lt;/b&gt;</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
  1. an id with a space is refused

    <PasswordField id="my password" label="Password" />

    error: a field id cannot contain spaces

  2. 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.

## Notices

Portions derived from GOV.UK Frontend (https://github.com/alphagov/govuk-frontend), Copyright (c) 2017 Crown Copyright (Government Digital Service), under the MIT License; the full notice is in NOTICE.

1.0.1 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE1,223
README.md5,007
impl/typescript/password_checklist.ts1,499
impl/typescript/password_field.tsx4,647
vectors.json21,282