Password Reveal Toggle
An eye button sits at the end of the password field. Tap it and the eye blinks open as the password shows, the dots loosening into letters. Tap again and it squeezes shut, lashes drawing in, as the password hides.
How to use it
- Paste the snippet just before
</head>. - Wrap a password input in an element with
data-password-reveal-toggle. - The eye button is added over the right end of the field, with room made for it.
Options
Add any of these attributes to the same element.
data-color | Eye colour. Default #52525b. |
data-shown | Start with the password showing. |
data-autoplay | It toggles itself every couple of seconds while in view, and stops as soon as you use it. |
The eye is a real button with aria-pressed. Listen for the toggle event: e.detail.shown is true or false.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Password Reveal Toggle: an eye button sits in the password field. Tap it
// and the eye blinks open as the password shows, the dots loosening into
// letters; tap again and it squeezes shut, lashes drawing in, as the
// password hides. It's a real toggle button (aria-pressed follows it).
// Usage: <div data-password-reveal-toggle><input type="password"></div>
// The button is added inside the wrapper, over the right end of the field.
// Optional: data-color (eye colour, default #52525b), data-shown (start
// with the password showing), data-autoplay (it toggles itself every couple
// of seconds while in view; it stops as soon as you use it).
// Listen for the "toggle" event: e.detail.shown is true or false.
var DEFAULTS = { color: '#52525b', every: 2000 };
var EYE =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<g class="prt-open"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle class="prt-pupil" cx="12" cy="12" r="3"/></g>' +
'<g class="prt-shut"><path class="prt-lid" d="M2.5 10.5c2.6 3.6 5.8 5.4 9.5 5.4s6.9-1.8 9.5-5.4"/>' +
'<path class="prt-lash" d="M6.6 14.3l-1.5 2.3"/><path class="prt-lash" d="M12 16v2.8"/><path class="prt-lash" d="M17.4 14.3l1.5 2.3"/></g></svg>';
var css =
'.prt{position:relative}' +
'.prt-btn{position:absolute;top:50%;right:0.35em;display:grid;place-items:center;width:2.2em;height:2.2em;margin-top:-1.1em;padding:0;border:0;border-radius:0.6em;' +
'background:none;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
'.prt-btn:hover{background:rgba(0,0,0,0.05)}' +
'.prt-btn:focus-visible{outline:2px solid currentColor;outline-offset:1px}' +
'.prt-btn svg{width:1.35em;height:1.35em;overflow:visible}' +
'.prt-btn g,.prt-btn circle{transform-box:fill-box;transform-origin:50% 50%}' +
'.prt-text{white-space:pre}' +
'.prt input::-ms-reveal,.prt input::-ms-clear{display:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function on(v) { return v !== undefined && v !== 'false'; }
function setup(el) {
var d = el.dataset;
var input = el.querySelector('input');
var field = input || el.querySelector('[data-field]') || el.firstElementChild;
if (!field) return;
var real = !!input;
el.classList.add('prt');
// In a card thumb (aria-hidden) there must be no real button.
var mock = !real || !!el.closest('[aria-hidden="true"]');
var btn = document.createElement(mock ? 'span' : 'button');
btn.className = 'prt-btn';
btn.style.color = d.color || DEFAULTS.color;
btn.innerHTML = EYE;
if (!mock) {
btn.type = 'button';
btn.setAttribute('aria-label', 'Show password');
if (input.id) btn.setAttribute('aria-controls', input.id);
}
el.appendChild(btn);
// Leave room for the button at the end of the field.
var pad = parseFloat(getComputedStyle(field).paddingRight) || 0;
var need = btn.offsetWidth + 8;
if (pad < need) field.style.paddingRight = need + 'px';
var text = null, secret = '';
if (!real) {
text = document.createElement('span');
text.className = 'prt-text';
secret = field.textContent.trim();
field.textContent = '';
field.appendChild(text);
}
var open = btn.querySelector('.prt-open');
var pupil = btn.querySelector('.prt-pupil');
var shut = btn.querySelector('.prt-shut');
var lashes = anime.svg.createDrawable(btn.querySelectorAll('.prt-lash'));
var lid = anime.svg.createDrawable(btn.querySelector('.prt-lid'));
var s = { shown: on(d.shown) };
function render() {
if (real) input.type = s.shown ? 'text' : 'password';
else text.textContent = s.shown ? secret : secret.replace(/./g, '•');
if (!mock) btn.setAttribute('aria-pressed', s.shown);
}
function still() {
anime.utils.set(open, { scaleY: s.shown ? 1 : 0, opacity: s.shown ? 1 : 0 });
anime.utils.set(pupil, { scale: 1 });
anime.utils.set(shut, { opacity: s.shown ? 0 : 1, y: 0 });
anime.utils.set(lid, { draw: '0 1' });
anime.utils.set(lashes, { draw: s.shown ? '0 0' : '0 1' });
}
function toggle() {
s.shown = !s.shown;
render();
el.dispatchEvent(new CustomEvent('toggle', { bubbles: true, detail: { shown: s.shown } }));
if (reduced()) { still(); return; }
if (s.shown) {
// Lashes tuck away, then the eye blinks open and the pupil pops.
anime.animate(lashes, { draw: '0 0', duration: 140, ease: 'in(2)' });
anime.animate(shut, { opacity: 0, duration: 120, delay: 100 });
anime.animate(open, { scaleY: [0.05, 1], opacity: [1, 1], duration: 520, delay: 110, ease: 'outElastic(1, .6)' });
anime.animate(pupil, { scale: [0.3, 1], duration: 500, delay: 230, ease: 'outBack(2.5)' });
anime.animate(field, { letterSpacing: ['0.3em', '0em'], duration: 420, ease: 'out(3)' });
} else {
// The eye squeezes shut, the lid settles and the lashes draw in.
anime.animate(open, { scaleY: 0.05, duration: 170, ease: 'in(3)' });
anime.animate(open, { opacity: 0, duration: 60, delay: 160 });
anime.animate(shut, { opacity: [1, 1], y: ['-0.12em', '0em'], duration: 380, delay: 150, ease: 'outBack(2)' });
anime.animate(lid, { draw: ['0.5 0.5', '0 1'], duration: 220, delay: 150, ease: 'out(2)' });
anime.animate(lashes, { draw: ['0 0', '0 1'], duration: 260, delay: anime.stagger(50, { start: 260 }), ease: 'out(2)' });
}
}
render();
still();
btn.addEventListener('click', function () { if (!mock) { stop(); toggle(); } });
if (real) {
input.addEventListener('pointerdown', stop);
input.addEventListener('keydown', stop);
}
var timer = null, stopped = false;
function stop() { stopped = true; clearTimeout(timer); }
if (on(d.autoplay) && !reduced()) {
var tick = function () {
if (stopped) return;
toggle();
timer = setTimeout(tick, DEFAULTS.every);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (!stopped && entries[0].isIntersecting) timer = setTimeout(tick, 900);
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-password-reveal-toggle]').forEach(function (el) {
if (el._prt) return;
el._prt = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-password-reveal-toggle>
<input type="password" aria-label="Password">
</div>