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 firstinputinside it viaquerySelector() - Flips that input's
typebetweenpasswordandtext - Sets
aria-pressedto"true"when the password is now visible,"false"when it's hidden again - Sets
aria-labelto"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 viaclosest('.password-field')thenquerySelector('input'), and does nothing if either lookup fails - Set the initial
aria-label="Show password"andaria-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 inaria-pressedandaria-label, and the icon swap only reinforces it visually autocompleteon the input should still reflect its purpose (current-password,new-password) — the toggle changes the input'stype, 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 totextin response to a click - Set the toggle's starting
aria-label/aria-pressedin the markup, since the script doesn't initialise them - Include both icons,
eye-offbeforeeye— 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 installingpassword-toggle.jsfrom 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>