A typical password field shows a coloured bar that turns from red to green and a list of rules whose ticks change colour as you type — which tells a colour-blind user nothing, tells a screen-reader user nothing unless it announces on every keystroke, and when it does announce on every keystroke makes the field nearly unusable.

Password guidance is valuable, and it can be accessible: show the requirements before the user starts, track each one in text as well as colour, announce only when the overall status changes, and let users see what they typed. This page, part of ARIA live regions for form errors, builds that component and draws the line between helpful requirements and rules that make passwords worse.


Context and prerequisites

Two kinds of feedback, often conflated:

  • Requirements — hard rules the password must meet to be accepted (“at least 12 characters”, “not a password found in known breaches”). These are validation: pass or fail.
  • Strength — an estimate of how guessable the password is, typically from an estimator such as zxcvbn. This is advice, not validation.

Current guidance (for example NIST SP 800-63B) favours length minimums, checking against breached-password lists, and allowing all printable characters and pasting — while discouraging composition rules like “must include a symbol” and periodic forced changes. Fewer, better rules also make the accessible UI simpler: there is less to track and announce.

Requirement rules worth having A minimum length such as twelve characters is recommended because length is the strongest factor. Checking against breached password lists is recommended because it blocks passwords attackers already try. Allowing all characters including spaces and emoji is recommended so passphrases and managers work. Allowing paste is recommended because password managers depend on it. Composition rules requiring symbols or digits are not recommended because they lead to predictable patterns. Maximum lengths below sixty-four characters are not recommended because they break generated passwords. Rule Use? Why minimum length (e.g. 12) yes length matters most not in breached-password lists yes blocks passwords attackers try first allow all characters, spaces yes passphrases, managers allow paste yes password managers need it must include symbol / digit no predictable patterns (Passw0rd!) max length under 64 no breaks generated passwords

The core pattern: a text-first checklist with status-change announcements

<label for="new-password">Create a password</label>
<p id="pw-reqs-intro">Your password must:</p>
<ul id="pw-reqs" aria-labelledby="pw-reqs-intro">
  <li data-rule="length"><span class="state">Not met: </span>be at least 12 characters</li>
  <li data-rule="breach"><span class="state">Not checked: </span>not be a commonly used password</li>
</ul>
<div class="pw-row">
  <input id="new-password" name="new-password" type="password" autocomplete="new-password"
         aria-describedby="pw-reqs-intro pw-reqs pw-strength">
  <button type="button" id="pw-toggle" aria-pressed="false" aria-controls="new-password">Show password</button>
</div>
<p id="pw-strength">Strength: <span class="strength-text">not rated yet</span></p>
type RuleState = "met" | "not-met" | "unchecked";

export function wirePasswordField(
  input: HTMLInputElement,
  list: HTMLElement,
  strengthText: HTMLElement,
  say: (msg: string) => void,                 // the page's announcer, not a local live region
  estimate: (pw: string) => 0 | 1 | 2 | 3 | 4,
) {
  let lastAllMet = false;
  let lastScoreBand = "";

  const setRule = (rule: string, state: RuleState) => {
    const li = list.querySelector<HTMLElement>(`[data-rule="${rule}"]`)!;
    li.dataset.state = state;                  // CSS adds colour and an icon
    li.querySelector(".state")!.textContent =
      state === "met" ? "Met: " : state === "not-met" ? "Not met: " : "Not checked: ";
  };

  input.addEventListener("input", () => {
    const pw = input.value;
    setRule("length", [...pw].length >= 12 ? "met" : "not-met");   // count graphemes-ish, not UTF-16 units
    // The breach check runs on blur (network); mark it unchecked while typing.
    setRule("breach", "unchecked");

    const score = pw ? estimate(pw) : null;
    const band = score === null ? "not rated yet" : ["very weak", "weak", "fair", "strong", "very strong"][score];
    strengthText.textContent = band;

    // Announce ONLY when the overall status crosses a threshold, never per keystroke.
    const allMet = [...pw].length >= 12;
    if (allMet !== lastAllMet) { say(allMet ? "Length requirement met." : "Password must be at least 12 characters."); lastAllMet = allMet; }
    if (band !== lastScoreBand && (band === "strong" || band === "very strong")) say(`Password strength: ${band}.`);
    lastScoreBand = band;
  });
}

// Show/hide toggle: a real button whose pressed state is exposed.
export function wireToggle(button: HTMLButtonElement, input: HTMLInputElement) {
  button.addEventListener("click", () => {
    const show = input.type === "password";
    input.type = show ? "text" : "password";
    button.setAttribute("aria-pressed", String(show));
    button.textContent = show ? "Hide password" : "Show password";
    input.focus();                              // keep the user's place
  });
}

Step-by-step walkthrough

  1. Show requirements before the user types. A list under the label, referenced by the input’s aria-describedby, so screen-reader users hear the rules when they reach the field — not only after failing.
  2. Write each rule’s state as text. “Met:”, “Not met:”, “Not checked:” prefixes (visually styled or hidden) carry the state; colour and icons reinforce it, per error styling that does not rely on colour.
  3. Update the list silently while typing. Users who want the current state read it by navigating the list or re-focusing the field.
  4. Announce only threshold crossings. “Length requirement met” once, when it flips — via the page announcer from throttling live region announcements, which also waits for typing pauses.
  5. Present strength as advice in text. “Strength: fair” next to any bar; do not block submission on strength unless it is a stated requirement.
  6. Provide a show-password toggle. A button with aria-pressed, returning focus to the input. It helps everyone check what they typed and removes the need for a “confirm password” field.
  7. Use autocomplete="new-password". Password managers then offer to generate a password, which will meet any sensible requirements.

Why a “confirm password” field is often unnecessary

The confirmation field exists to catch typos in a hidden password. A show-password toggle achieves the same with less effort: users can look at what they typed, once. Password managers fill both fields identically anyway, so the confirmation adds nothing for them, while for people using switch access, voice control or screen magnification it doubles the work of the hardest field on the form. Keep confirmation only where a typo would be expensive to recover from and there is no reset route — and if you keep it, validate the match as described in password confirmation validation pattern.

What a screen-reader user hears On focusing the field, the user hears the label, then the description including your password must, be at least twelve characters, not met, and not be a commonly used password, not checked, and strength not rated yet. While typing, nothing is announced apart from keystroke echo. After the twelfth character and a short pause, the announcer says length requirement met, once. On blur, the breach check runs and its result updates the list; if the password is found, an error is shown and announced. Focus the field Label + requirements + strength. Rules heard before typing starts. Typing List updates silently. Only keystroke echo is heard. 12th character + pause "Length requirement met." Announced once, on the threshold. Blur Breach check runs. Result updates the list; a failure becomes the field error.

Failure modes and edge cases

1. Announcing on every keystroke

A live region that reads the whole checklist per character makes the field unusable with a screen reader. Update silently; announce threshold crossings only.

2. Blocking paste

Disabling paste (onpaste="return false") breaks password managers and forces users to type long passwords by hand, pushing them toward weaker ones. Never block paste.

3. Counting characters incorrectly

"🔑".length is 2 in JavaScript. Counting with [...pw].length (code points) is closer to what users see; for full accuracy with combined emoji, use Intl.Segmenter by grapheme. Make the server count the same way.

4. Breach checks leaking the password

Never send the password itself to a third-party service. Range-query APIs based on k-anonymity (sending only a hash prefix) let you check breach lists without revealing the password; better still, perform the check on your server during submit.

5. Strength meters that contradict the rules

A password that meets every requirement but shows “Weak” in red confuses users. Align them: if strength is advisory, word it as advice (“Could be stronger: add more words”), and never style advisory feedback like an error.

Requirement, advice, error The requirement checklist is visible before typing, updated silently, and uses text states such as met and not met. Strength advice is shown as text such as strength fair, with an optional bar, and never blocks submission unless it is a stated requirement. A validation error appears on blur or submit when a requirement is not met, is linked with aria-describedby and sets aria-invalid. Requirements Visible before typing. Text states: Met / Not met. Strength advice "Strength: fair" in text. Never styled as an error. Validation error On blur or submit. aria-invalid + describedby.

Verification checklist


Frequently Asked Questions

Should the strength meter use role="progressbar" or meter?

A <meter> element fits a strength gauge semantically, but its value is announced inconsistently. The most reliable approach is visible text (“Strength: fair”) referenced by the input’s description, with the bar as decoration.

Is zxcvbn too large to load on every page?

It is large because it contains dictionaries. Load it lazily when the password field receives focus, or run the estimate on the server. The checklist and length rule work without it in the meantime.

How do I phrase the breached-password error?

Say what to do without alarming: “This password has appeared in a data breach elsewhere, so it’s easy to guess. Choose a different one.” Avoid implying the user’s own account was breached.


Related

← ARIA Live Regions for Form Errors