⚠️ Design system CSS not found. Check the path in cms/docs.config.js → designSystemPath, then re-run npm run docgen.

Password Toggle

Show/hide control for password input fields

Website / Password Toggle
Download .md file
Open .md in new tab
On this page
  • Basic usage
  • JavaScript
  • Accessibility
  • Usage rules
  • CSS reference
  • Use in another product

The password toggle adds a show/hide button to a password input, letting a user reveal what they typed without a second confirmation field. It requires the wrapper around the input and toggle button, and the toggle button itself uses the class. The component requires JavaScript (assets/js/password-toggle.js).

The button carries both state icons — eye-off while the password is hidden, eye once revealed. CSS swaps them off the button's aria-pressed attribute, so the script never touches the icon.


Basic usage

Include the script on any page that uses password toggles:

<script src="/assets/js/password-toggle.js" defer></script>

The script binds one delegated click listener to the document. No manual setup is needed, and it does not matter when the markup arrives.

<div class="password-field">
  <input type="password" id="password" name="password" autocomplete="current-password" placeholder="Enter your password">
  <button type="button" class="password-toggle" aria-label="Show password" aria-pressed="false">
    <div class="svg-icn" data-icon="eye-off">
      <!-- inline eye-off SVG -->
    </div>
    <div class="svg-icn" data-icon="eye">
      <!-- inline eye SVG -->
    </div>
  </button>
</div>

Put eye-off first: it is the icon for the initial hidden state. Both drawings come from the icon registry (eye, eye-off).


JavaScript

What the script does

On click, the toggle:

  • Resolves the clicked element to a via closest(), so clicking the icon inside the button counts as clicking the button
  • Finds its ancestor via closest(), then the first input inside it via querySelector()
  • Flips that input's type between password and text
  • Sets aria-pressed to "true" when the password is now visible, "false" when it's hidden again
  • Sets aria-label to "Hide password" when visible, "Show password" when hidden

The script never touches the icon. The visual swap — eye-off while hidden, eye while revealed — is pure CSS keyed on aria-pressed, which is why the markup carries both icons. Legacy markup with a single icon still works: the CSS scopes the swap to buttons that contain an eye-off icon, so a lone eye stays static rather than disappearing. Accessible state lives in aria-pressed, aria-label, and the revealed text either way.

Late-arriving markup needs nothing

The listener sits on the document and resolves the target on each click, so it never holds a reference to a button. A toggle rendered at load, swapped in by a Barba transition, or injected by a script afterward all behave identically, and there is no re-initialisation step.

window.initPasswordToggle is exposed because an exported init is part of the module contract — a consumer bundling this file rather than loading it with a <script> tag needs an entry point. It is guarded, so calling it twice binds nothing twice. It is deliberately not registered in bd-site-init.js and does not need to be: a delegated listener installs itself once and no navigation removes it. dropdown.js and dialog.js are absent from that dispatcher for the same reason.

This replaced a version that queried every at load and bound each one directly. On a Barba site that shape is a trap: head scripts do not re-run on navigation, so a toggle worked on a full page load and was inert when the same page was reached by clicking a link — see js-code-structure.md.


Accessibility

  • The button must sit inside a wrapper alongside its input — the script locates the input via closest('.password-field') then querySelector('input'), and does nothing if either lookup fails
  • Set the initial aria-label="Show password" and aria-pressed="false" in markup — the script never sets them, it only updates them after a click
  • Use a <button type="button">, never a bare <div> or <a> — this gives native keyboard support (Enter/Space) and stops the button from submitting the form
  • Mark both icons aria-hidden="true" — they're decorative; the state information lives in aria-pressed and aria-label, and the icon swap only reinforces it visually
  • autocomplete on the input should still reflect its purpose (current-password, new-password) — the toggle changes the input's type, not its autofill behaviour

Usage rules

Do:

  • Use on password inputs where letting a user verify what they typed reduces failed submits — login, account settings, password reset forms
  • Give every password field its own wrapper — each toggle manages exactly one input independently
  • Keep the input's initial type="password" in markup; the script only flips it to text in response to a click
  • Set the toggle's starting aria-label/aria-pressed in the markup, since the script doesn't initialise them
  • Include both icons, eye-off before eye — the swap needs both present, and putting the initial-state icon first keeps the fallback sensible where the CSS doesn't load

Don't:

  • Don't place a outside a wrapper — the click handler can't locate the input and silently does nothing
  • Don't swap the icon from JavaScript — the CSS already does it off aria-pressed; a script that also edits the icon fights the stylesheet
  • Don't add a second handler that also flips input.type — the delegated listener already does it, and the two would cancel each other out. Handlers that do something else on the same click (analytics, validation) are fine

CSS reference

This section documents how the component is built. For usage, see the sections above.

Styling

Property Value
position: relative — establishes the positioning context the toggle docks into
.password-field input padding-right: var(--space-4xl) — clears space so typed text doesn't run under the toggle
Absolutely positioned, vertically centred, right: var(--space-m), no background or border
colour , shifting to on hover
.password-toggle .svg-icn 20×20px

Selectors

Selector Purpose
Wrapper; positions the toggle relative to the input
.password-field input The password input; gets reserved right padding
The show/hide button
.password-toggle:hover Hover colour state
.password-toggle .svg-icn Icon sizing
.password-toggle:has(.svg-icn[data-icon="eye-off"]):not([aria-pressed="true"]) .svg-icn[data-icon="eye"] Hides the eye icon while the password is hidden — scoped by :has() to dual-icon toggles so legacy single-icon markup keeps its static icon
.password-toggle[aria-pressed="true"] .svg-icn[data-icon="eye-off"] Hides the eye-off icon once the password is revealed

These rules live in design-system.css (section 12, Forms), so a product installing password-toggle.js from the package gets a working control — behaviour and styling together.


Use in another product

The design system installs once per product:

npm install github:bydefaultstudio/design-system-dist#semver:^4.7.0

This component's styles ship in design-system.css. Its behaviour ships as dist/js/password-toggle.js — copy it into the product's served assets and include it once per page:

<script src="assets/js/password-toggle.js" defer></script>
On this page
  • Basic usage
  • JavaScript
  • Accessibility
  • Usage rules
  • CSS reference
  • Use in another product
Previous Slider
Next Callout

Was this page helpful?

We use this feedback to improve our documentation.

Thanks for your feedback

Send feedback

© 2026 By Default