How to Implement a Password Reveal

Passwords are becoming more complex, making it easier to make a typo. Edge added a password reveal feature that is quite handy.

Obfuscated Password
Obfuscated Password
Revealled Password
Revealled Password

The basic idea is simple: add a button that switches the input's type between password and text when the user clicks on it. Edge's feature is a little more sophisticated as the reveal button is visible if and only if the input was empty any time since the input got the focus, as the following flowchart explains it:

graph TD A[Input] --> C[canReveal:=false] C -.->|onfocus| B{Check value} B --> |length>0| F(Listen) B --> |length=0| D(canReveal:=true) F -.-> |onblur| E(End) F -.-> |oninput| B B --> |canReveal & length>0| I(Show Reveal cue) I --> J(Listen) D --> F(Listen) J -.-> |on reveal| G(Show password) G --> F J -.-> |on unreveal| H(Obfuscate password) H --> F

Implement the behavior

To implement this, you need an HTML element serving as a visual cue to click on to reveal/unreveal the password, and a wrapper element to setup the display. Then, you just need a few attributes to store the states and some event listener to register.

document.querySelectorAll('input[type=password]').forEach(function (input) {
    const cue = document.createElement("span");
    cue.setAttribute('aria-hidden', 'true');

    const wrapper = document.createElement("span");
    wrapper.classList.add('input-wrapper');

    input.parentNode.insertBefore(wrapper, input);
    wrapper.appendChild(input);
    wrapper.appendChild(cue);

    let reveal = false;
    const checkReveal = function () {
        input.dataset.reveal = Boolean(reveal |= !input.value);
        input.dataset.valueLength = input.value.length;
    };

    input.addEventListener('focus', function () {
        input.addEventListener('input', checkReveal);
        input.dataset.reveal = Boolean(reveal = !input.value);
        input.dataset.valueLength = input.value.length;
    }, { passive: true });
    input.addEventListener('blur', function () {
        input.setAttribute('type', 'password');
        input.dataset.reveal = reveal = false;
        input.removeEventListener('input', checkReveal);
    }, { passive: true });

    cue.addEventListener('mousedown', function (e) {
        e.preventDefault();
        const type = input.getAttribute('type') === 'password' ? 'text' : 'password';
        input.setAttribute('type', type);
    });
});

The attribute data-reveal indicates whether the reveal button should be displayed, and the data-value-length contains the length of the password. When the password length is 0, then data-reveal is set to true and when the length is not 0 anymore, the button is made visible.

On the cue, the mousedown event, with default behavior prevented, toggles the reveal of the password. This way, we can prevent the input from loosing the focus. it is not possible on a click event.

The other listeners are passive, as they do not call preventDefault.

Then style the widget

The .input-wrapper element set the position property to relative, so the visual cue for toggling can be positionned absolutely.

Then it is simply a matter of setting the necessary properties to display the appropriate icons, based on the state of the input .

Note that the following rule hides the native edge cues when the input has been setup with our reveal behavior.

input[data-reveal]::-ms-reveal {
  display: none;
}

The following scss code can be used to display the visual cues:

.input-wrapper {
    position: relative;
    display: block;
}

input[data-reveal]::-ms-reveal, input[data-reveal] + [aria-hidden] {
    display: none;
}

input[data-reveal=true]:not([data-value-length='0']) {
    padding-right: calc(#{$fa-fw-width} + 1rem);

    & + [aria-hidden] {
        speak: none;
        font-family: FontIcons;
        font-weight: normal;
        box-sizing: content-box;
        padding: 0.25rem 0.5rem;
        height: calc(100% - 0.5rem);
        width: $fa-fw-width;
        position: absolute;
        top: 0;
        right: 0;
        display: flex;
        justify-content: center;
        align-content: center;
        flex-direction: column;
        z-index: 1000;
    }

    &[type=password] + [aria-hidden]::before {
        content: fa-content($fa-var-eye);
    }

    &[type=text] + [aria-hidden]:before {
        content: fa-content($fa-var-eye-slash);
    }
}

The right-padding of the input, as well as the z-index of the visual cue, have been set to make room for the visual cue and keep it visible when the input has the focus.

Also, the following rules are set to center the icon vertically and horizontally:

  display: flex;
  justify-content: center;
  align-content: center;
  flex-direction: column;
  z-index: 1000;

On a closing note, there are two noticeable differences with the Edge's password reveal:

  1. The password does not reveal on Alt+F8
  2. The input keeps the focus when clicking on the visual cue.

Also, like Edge's password reveal, the input does not detect the password as reveallable when you select all the text and replaces it. You have to empty the input first.

Leave a comment

Please note that we won't show your email to others, or use it for sending unwanted emails. We will only use it to render your Gravatar image and to validate you as a real person.