Overview
Password show/hide toggles let users check what they’ve typed in a password field. The pattern is deceptively complex for accessibility: toggling the input type between password and text must be communicated to screen reader users without reading the actual password aloud. An icon-only button with no accessible name, no state management, and no status announcement leaves assistive technology users completely in the dark — or worse, exposes their password through a live region.
WCAG Criteria:
- 4.1.2 Name, Role, Value — the toggle button must have an accessible name and expose its pressed state
- 4.1.3 Status Messages — the visibility change must be announced without moving focus and without reading the password
Key requirements:
- The toggle button must have an accessible name (“Show password”) — icon-only buttons with no label are invisible to screen readers
- Use
aria-pressedto communicate whether the password is currently shown or hidden - Announce the state change (“Password is now visible” / “Password is now hidden”) via an
aria-live="polite"region - Never place the password value itself inside a live region — that would read it aloud
- Never change both the label AND
aria-pressedat the same time — pick one mechanism for communicating state - Return focus to the password input after toggling so the user can continue typing
- The GOV.UK pattern is the gold standard for this interaction
Password Toggle
Icon-Only Eye Button vs. Accessible Show/Hide Toggle
<!-- Icon-only button — no name, no state, no announcement -->
<div>
<div>Password</div>
<input id="pw-bad-input" type="password" />
<button onclick="
var i = document.getElementById('pw-bad-input');
i.type = i.type === 'password' ? 'text' : 'password';
">
👁 <!-- eye icon only -->
</button>
</div><!-- Labeled button + aria-pressed + live region announcement -->
<label for="pw-good-input">Password</label>
<input id="pw-good-input" type="password" />
<button type="button"
aria-pressed="false"
onclick="
var input = document.getElementById('pw-good-input');
var pressed = this.getAttribute('aria-pressed') === 'true';
var sr = document.getElementById('pw-sr');
if (!pressed) {
input.type = 'text';
this.setAttribute('aria-pressed', 'true');
sr.textContent = 'Password is now visible';
} else {
input.type = 'password';
this.setAttribute('aria-pressed', 'false');
sr.textContent = 'Password is now hidden';
}
input.focus();
">
Show password
</button>
<!-- Announces state change without reading the password -->
<span id="pw-sr" aria-live="polite" class="sr-only"></span>What’s wrong with the icon-only eye button?
- No accessible name — screen readers announce “button” with no indication of what it does
- No
aria-pressedstate — screen readers cannot tell whether the password is currently visible or hidden - No status announcement — toggling the input type produces no feedback for assistive technology users
- The eye emoji/icon is the only affordance — completely invisible to screen readers
- The
<div>Password</div>is not a<label>— the input has no programmatic label - Focus stays on the button after toggle, forcing the user to Tab back to the input
What the screen reader announces:
| Version | Announcement |
|---|---|
| Inaccessible (icon-only button) | “button” (no name, no state, no feedback on toggle) |
Accessible (aria-pressed + live region) | “Show password, toggle button, not pressed” then on toggle: “Password is now visible” |
Critical: never announce the password itself. If you put the password input’s value into a live region, the screen reader will read the password aloud — a serious privacy and security issue, especially in shared spaces.
Why the label stays “Show password”:
- The
aria-pressedattribute communicates the current state (pressed = visible, not pressed = hidden) - Changing the label to “Hide password” while also flipping
aria-pressedsends conflicting signals - A stable label helps speech recognition users who need to say the button name consistently