View as Markdown

Password Visibility

A Stimulus behavior that shows the password typed in a field as plain text, or hides it again, from a toggle button.

100%
Loading...
<div data-controller="password-visibility" class="">
    <label for="password" class="">Password</label>
    <div class="">
        <input type="password" id="password" value="my-super-secret-password" placeholder="Your password" autocomplete="current-password" spellcheck="false" autocapitalize="off" data-password-visibility-target="input" class="">
        <button type="button" aria-controls="password" data-action="password-visibility#toggle" class="">
            <svg data-password-visibility-target="show" role="img" aria-label="Show password" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class=""><path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
            <svg data-password-visibility-target="hide" hidden role="img" aria-label="Hide password" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class=""><path d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/></svg>
        </button>
    </div>
</div>

Installation

Note

Available since UX Toolkit 3.7.

php bin/console ux:install password-visibility --kit common

Copy the following file(s) into your app:

assets/controllers/password_visibility_controller.js
import { Controller } from '@hotwired/stimulus';

/**
 * @target  input   Password field that the controller switches between `password` and `text`. It goes back to `password` when its form is submitted or reset, and when the page is left.
 * @target  show    Elements shown while the password is hidden, like an "eye" icon or a "Show" label.
 * @target  hide    Elements shown while the password is visible, like an "eye off" icon or a "Hide" label.
 * @action  toggle  Shows the password as plain text, or hides it again. After a click, the field gets the focus back.
 */
export default class extends Controller {
    static targets = ['input', 'show', 'hide'];

    #listeners;

    connect() {
        // Turbo restores a page from its cache with the password as it was, visible or not.
        this.#setVisible(false);

        // Once submitted as plain text, a password could join the values the browser suggests for text fields.
        this.#listeners = new AbortController();
        const hide = () => this.#setVisible(false);
        const { form } = this.inputTarget;
        form?.addEventListener('submit', hide, { signal: this.#listeners.signal });
        form?.addEventListener('reset', hide, { signal: this.#listeners.signal });
        // Keeps the back/forward cache from restoring the page with the password visible.
        window.addEventListener('pagehide', hide, { signal: this.#listeners.signal });
    }

    disconnect() {
        this.#listeners.abort();
    }

    toggle(event) {
        // Keeps a button without a type from submitting its form.
        event.preventDefault();

        this.#setVisible('password' === this.inputTarget.type);

        // A click made with the keyboard has a `detail` of 0: the focus then stays on the button.
        if (event.detail > 0) {
            this.inputTarget.focus();
        }
    }

    #setVisible(visible) {
        this.inputTarget.type = visible ? 'text' : 'password';
        // An `<svg>` has no `hidden` property, only the attribute.
        this.showTargets.forEach((element) => element.toggleAttribute('hidden', visible));
        this.hideTargets.forEach((element) => element.toggleAttribute('hidden', !visible));
    }
}

Usage

Add data-controller="password-visibility" around the password field and its toggle button. Mark the field as the input target, and trigger password-visibility#toggle from the button:

<div data-controller="password-visibility">
    <input type="password" id="password" data-password-visibility-target="input" spellcheck="false" autocapitalize="off">
    <button type="button" aria-controls="password" data-action="password-visibility#toggle">
        <span data-password-visibility-target="show">Show</span>
        <span data-password-visibility-target="hide" hidden>Hide</span>
    </button>
</div>

Each click switches the field between password and text, and shows either the show targets or the hide targets. An <svg> only hides with the hidden attribute when your CSS does it, as Tailwind CSS and Bootstrap do.

Examples

Text Labels

The show and hide targets can hold any content, like a text label.

100%
Loading...
<div data-controller="password-visibility" class="">
    <label for="new-password" class="">New password</label>
    <div class="">
        <input type="password" id="new-password" value="correct horse battery staple" autocomplete="new-password" spellcheck="false" autocapitalize="off" data-password-visibility-target="input" class="">
        <button type="button" aria-controls="new-password" data-action="password-visibility#toggle" class="">
            <span data-password-visibility-target="show">Show</span>
            <span data-password-visibility-target="hide" hidden>Hide</span>
        </button>
    </div>
</div>

API Reference

data-controller="password-visibility"

Target Description
input Password field that the controller switches between password and text. It goes back to password when its form is submitted or reset, and when the page is left.
show Elements shown while the password is hidden, like an "eye" icon or a "Show" label.
hide Elements shown while the password is visible, like an "eye off" icon or a "Hide" label.
Action Description
toggle Shows the password as plain text, or hides it again. After a click, the field gets the focus back.