GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Show Password Toggle Icon: 9 Reveal Patterns

Nine show password toggle icon builds: One eye icon opens onto nine forking alleys.

Auto-plays · click a tile to jump to its section · all nine in one zip

Show Password Toggle Icon: How One Input Type Value Changes Nine Ways

All nine builds below answer the same question a different way: what should happen the moment a password field's masked state flips? Item 01 keeps the change to one field's own overlay. By item 03 that same change is shared across two fields at once, item 05 and item 06 borrow the field for something other than masking, and item 09 folds three of the earlier mechanisms into a single signup form. The order moves from a single field reacting to itself toward several fields and functions reacting to one shared value together.

01Eye Icon That Morphs Letters and Dots in Place

buildOverlay() builds one pem__ch wrapper per character, stacking a masked pem__mask dot and the real pem__char letter in the exact same grid cell instead of swapping two separate elements in and out, and animateEyeStep() eases a single eyeProgress value toward 0 or 1 over EYE_MORPH_MS while paintEye() reads that value to bend both eyelid paths and fade the pupil at the same time.

Eye Icon That Morphs Letters and Dots in Place
Open in a new tab

01 · Eye Icon That Morphs Letters and Dots in Place — Measured area 76.015% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

EYE_MORPH_MSbuildOverlaypem__ch
  function buildOverlay(value) {
    overlay.textContent = '';
    for (var i = 0; i < value.length; i += 1) {
      var ch = document.createElement('span');
      ch.className = 'pem__ch';
      var delay = (i * CHAR_STAGGER_MS) + 'ms';
      var mask = document.createElement('span');
      mask.className = 'pem__mask';
      mask.style.transitionDelay = delay;
      mask.textContent = String.fromCharCode(8226);
      var real = document.createElement('span');
      real.className = 'pem__char';
      real.style.transitionDelay = delay;
      real.textContent = value.charAt(i);
      ch.appendChild(mask);
      ch.appendChild(real);
      overlay.appendChild(ch);
    }
  }
  buildOverlay(FAKE_PW);

Item 01 repainted 76.0% of the frame at an average intensity of 44.6 across 14/23 measured frames.

02Warning Badge That Springs Up When Caps Lock Is On

onKey() reads the real KeyboardEvent.getModifierState('CapsLock') on every keydown and keyup and hands that boolean straight to setCapsLock(), which only ever changes one target number; stepSpring() integrates that target against pos and vel every frame, so CAPSLOCK_BADGE_OVERSHOOT_PX decides how far past its resting spot the badge springs before paintBadge() settles it back in place.

Warning Badge That Springs Up When Caps Lock Is On
Open in a new tab

02 · Warning Badge That Springs Up When Caps Lock Is On — Measured area 75.869% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

CAPSLOCK_BADGE_OVERSHOOT_PXstepSpringpcs__badge--active
  function stepSpring(ts) {
    if (lastT === null) lastT = ts;
    var dt = Math.min(0.032, (ts - lastT) / 1000);
    lastT = ts;
    var acc = (target - pos) * SPRING_STIFFNESS - vel * SPRING_DAMPING;
    vel += acc * dt;
    pos += vel * dt;
    paintBadge(pos);
    var settled = Math.abs(target - pos) < 0.002 && Math.abs(vel) < 0.01;
    if (!settled) {
      springRaf = requestAnimationFrame(stepSpring);
    } else {
      pos = target;
      paintBadge(pos);
      springRaf = null;
      lastT = null;
    }
  }

  function setCapsLock(on) {
    var next = on ? 1 : 0;
    if (target === next) return;
    target = next;
    badge.classList.toggle('pcs__badge--active', on);
    root.classList.toggle('pcs--warning', on);

Item 02 repainted 75.9% of the frame at an average intensity of 49.3 across 15/23 measured frames.

03Sync Toggle That Opens Several Fields at Once

One onToggle() handler flips a single shared revealed value for both the password and confirm-password fields, and paintEyes() writes that same eased progress into both eye icons' lid paths and both pupils inside one function call — SYNC_EYE_MORPH_MS is the only timing number either field ever reads, and neither field keeps a progress value of its own.

Sync Toggle That Opens Several Fields at Once
Open in a new tab

03 · Sync Toggle That Opens Several Fields at Once — Measured area 76.029% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

SYNC_EYE_MORPH_MSpaintEyespfs__hint
  function paintEyes(progress) {
    var eased = easeOutCubic(progress);
    var topY = 12 - 8 * eased;
    var botY = 12 + 8 * eased;
    var d1 = 'M2,12 Q12,' + topY + ' 22,12';
    var d2 = 'M2,12 Q12,' + botY + ' 22,12';
    lidTopA.setAttribute('d', d1);
    lidBottomA.setAttribute('d', d2);
    pupilA.style.opacity = String(eased);
    lidTopB.setAttribute('d', d1);
    lidBottomB.setAttribute('d', d2);
    pupilB.style.opacity = String(eased);
  }

  var eyeProgress = 0;
  var eyeRaf = null;
  var animT0 = null, animStart = 0, animTarget = 0;
  paintEyes(eyeProgress);

  function animateEyeStep(ts) {

Item 03 repainted 76.0% of the frame at an average intensity of 65.2 across 10/23 measured frames.

04Notice Banner That Slides Out When a Paste Is Detected

The field listens for a real paste event rather than counting keystrokes or watching for a sudden jump in length, and handlePaste() never reads event.clipboardData — the banner only ever confirms that something was pasted, not what. PASTE_BANNER_VISIBLE_MS holds the banner in place before PASTE_BANNER_SLIDE_MS times how long it takes to retreat.

Notice Banner That Slides Out When a Paste Is Detected
Open in a new tab

04 · Notice Banner That Slides Out When a Paste Is Detected — Measured area 10.463% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

PASTE_BANNER_SLIDE_MSPASTE_BANNER_VISIBLE_MSsimulatePaste
  function handlePaste() {
    clearTimeout(hideTimer);
    clearTimeout(clearTimer);
    banner.textContent = BANNER_MESSAGE;
    banner.classList.add('is-visible');
    hideTimer = setTimeout(function () {
      banner.classList.remove('is-visible');
      clearTimer = setTimeout(function () { banner.textContent = ''; }, PASTE_BANNER_SLIDE_MS);
    }, PASTE_BANNER_VISIBLE_MS);
  }

  input.addEventListener('paste', function () { handlePaste(); });

Item 04 repainted 10.5% of the frame at an average intensity of 57.4 across 12/23 measured frames.

05Highlight Glow That Spreads When the Browser Autofills

There's no autofill-detection API behind this one — a comment in the source says so directly — so fillAndGlow() is wired to a simulate button that fills the field and starts glowTick(), which multiplies a rising sine by an exponential decay every frame so AUTOFILL_GLOW_PULSE_MS controls exactly one pulse's length rather than a repeating loop.

Highlight Glow That Spreads When the Browser Autofills
Open in a new tab

05 · Highlight Glow That Spreads When the Browser Autofills — Measured area 23.938% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

AUTOFILL_GLOW_PULSE_MSfillAndGlowafg__input
  function glowTick(ts) {
    if (glowStartTs === null) glowStartTs = ts;
    var t = ts - glowStartTs;
    if (t > AUTOFILL_GLOW_PULSE_MS) {
      input.style.setProperty('--glow', '0');
      root.style.setProperty('--glow', '0');
      glowRaf = 0;
      glowStartTs = null;
      return;
    }
    var envelope = Math.max(0, Math.sin(Math.PI * t / AUTOFILL_GLOW_PULSE_MS)) * Math.exp(-2.2 * t / AUTOFILL_GLOW_PULSE_MS);
    input.style.setProperty('--glow', String(envelope));
    root.style.setProperty('--glow', String(envelope));
    glowRaf = requestAnimationFrame(glowTick);
  }

  function resetField() {
    input.value = '';
  }

Item 05 repainted 23.9% of the frame at an average intensity of 34.0 across 11/23 measured frames.

06Cascade That Types In a Generated Password Letter by Letter

generatePassword() draws its candidate string from a seedRand()-seeded pool built once at load, then stepCascade() accumulates elapsed time and reveals one more character every time that accumulator crosses GENERATE_CASCADE_STEP_MS; once every character is on screen, the same function switches to timing a COPY_MORPH_MS cross-fade into a checkmark, and no Clipboard API call sits behind that copy icon.

Cascade That Types In a Generated Password Letter by Letter
Open in a new tab

06 · Cascade That Types In a Generated Password Letter by Letter — Measured area 14.485% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

GENERATE_CASCADE_STEP_MSCOPY_MORPH_MSgeneratePassword
  function stepCascade(dtMs) {
    if (cascade.active) {
      cascade.acc += dtMs;
      while (cascade.acc >= GENERATE_CASCADE_STEP_MS && cascade.revealed < cascade.target.length) {
        cascade.acc -= GENERATE_CASCADE_STEP_MS;
        cascade.revealed += 1;
        display.textContent = cascade.target.slice(0, cascade.revealed);
      }
      if (cascade.revealed >= cascade.target.length) {
        cascade.active = false;
        cascade.morphing = true;
        cascade.morphAcc = 0;
      }
    } else if (cascade.morphing) {
      cascade.morphAcc += dtMs;
      var p = Math.min(1, cascade.morphAcc / COPY_MORPH_MS);
      root.style.setProperty('--copyMorph', String(p));
      if (p >= 1) {
        cascade.morphing = false;
        root.classList.add('is-copied');
      }
    }
  }

  genBtn.addEventListener('click', generatePassword);

Item 06 repainted 14.5% of the frame at an average intensity of 126.1 across 14/23 measured frames.

07Hold-to-Reveal With a Progress Ring While Pressed

startHold() fires on a real pointerdown and on a Space keydown alike, and loop() measures how long the button has been held against HOLD_DURATION_MS to fill the ring, flipping input.type to text only once that ratio reaches 1; releasing the pointer, dragging off the button, or letting go of the key all call the same endHold(), so there's exactly one way back to masked.

Hold-to-Reveal With a Progress Ring While Pressed
Open in a new tab

07 · Hold-to-Reveal With a Progress Ring While Pressed — Measured area 75.911% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

HOLD_DURATION_MSphrRingFgEYE_OPEN_MS
  function loop(ts) {
    if (!holding) return;
    var elapsed = ts - holdStart;
    var progress = Math.max(0, Math.min(1, elapsed / HOLD_DURATION_MS));
    setRing(progress);
    if (progress >= 1 && !revealed) {
      revealed = true;
      input.type = 'text';
      btn.setAttribute('aria-pressed', 'true');
    }
    var eyeProgress = revealed ? 1 : Math.max(0, Math.min(1, elapsed / EYE_OPEN_MS));
    setEye(eyeProgress);
    rafId = requestAnimationFrame(loop);
  }

  function startHold() {
    if (holding) return;
    holding = true;
    revealed = false;
    holdStart = performance.now();
    rafId = requestAnimationFrame(loop);
  }

  function endHold() {

Item 07 repainted 75.9% of the frame at an average intensity of 92.4 across 10/23 measured frames.

08Keyboard Path Where a Shortcut Toggle Brightens the Focus Ring

toggleReveal() is the only function that ever flips input.type, and both the eye button's click handler and a Ctrl+Shift+H keydown check call it before running flashFocusRing(), whose step() decays a --ring-alpha custom property over FOCUS_RING_FLASH_MS so the field itself, not only the button, visibly answers a keyboard shortcut with no pointer involved.

Keyboard Path Where a Shortcut Toggle Brightens the Focus Ring
Open in a new tab

08 · Keyboard Path Where a Shortcut Toggle Brightens the Focus Ring — Measured area 75.756% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

FOCUS_RING_FLASH_MSflashFocusRing--ring-alpha
  function toggleReveal() {
    var willShow = input.type === 'password';
    input.type = willShow ? 'text' : 'password';
    eyeBtn.setAttribute('aria-pressed', String(willShow));
    root.classList.toggle('is-open', willShow);
  }

  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var flashStart = 0;
  var flashRaf = null;
  var reduceFlashTimer = null;

  function step(ts) {
    var t = ts - flashStart;
    var envelope = Math.max(0, 1 - t / FOCUS_RING_FLASH_MS);
    input.style.setProperty('--ring-alpha', String(envelope));
    if (envelope > 0) {
      flashRaf = requestAnimationFrame(step);
    } else {
      flashRaf = null;
    }
  }

  function flashFocusRing() {

Item 08 repainted 75.8% of the frame at an average intensity of 29.0 across 10/23 measured frames.

09Hero Scene Combining Several Patterns in One Signup Form

heroSimulateAutofill() and heroRunCascade() are items 05 and 06's mechanisms renamed and pointed at the same form's password field instead of each getting its own demo field, running alongside item 01's morph through toggleEye() on one shared pwInput — HERO_CASCADE_STEP_MS is the only new constant the scene adds, everything else is the same three mechanisms composed rather than rebuilt from scratch.

Hero Scene Combining Several Patterns in One Signup Form
Open in a new tab

09 · Hero Scene Combining Several Patterns in One Signup Form — Measured area 75.046% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

HERO_CASCADE_STEP_MSheroSimulateAutofillheroMorphEye
  function heroSimulateAutofill() {
    glowStart = performance.now();
    if (glowRafId) return;
    glowRafId = requestAnimationFrame(glowStep);
  }

  var cascadeTimer = null;
  function heroRunCascade() {
    var target = pickPassword(9, cascadeRand);
    var i = 0;
    pwInput.type = 'text';
    eyeBtn.setAttribute('aria-pressed', 'true');
    animateEyeTo(1);
    pwInput.value = '';
    root.classList.remove('is-generated');
    window.clearTimeout(cascadeTimer);
    function step() {
      i += 1;
      pwInput.value = target.slice(0, i);
      if (i < target.length) {
        cascadeTimer = window.setTimeout(step, HERO_CASCADE_STEP_MS);
      } else {
        root.classList.add('is-generated');
        cascadeTimer = window.setTimeout(function () {

Item 09 repainted 75.0% of the frame at an average intensity of 75.9 across 16/23 measured frames.

Which Password Field Gets Which of These Nine

Every login form needs some kind of password show toggle, but which of these nine fits depends on what's actually at stake. Item 01's cross-fade eye suits an ordinary login field, and item 02's spring badge earns its keep the moment a signup form's password step needs to warn about an accidental Caps Lock before submission fails. Item 03's shared toggle belongs wherever a password and confirm-password pair sit side by side, and item 04's paste banner is built for a field that a password manager fills through a simulated paste rather than a real keystroke-by-keystroke entry. Item 05 confirms a saved credential landed in the right field, item 06 hands a signup flow its own generate-and-copy step, and item 07's hold-to-reveal fits a password shown only briefly on a shared or presentation screen. Item 08 answers exactly how to reveal password without ever touching a mouse, and item 09 folds several of these into one landing-page signup hero. For the strength feedback that usually sits right below a field like this, Password Strength Meter UI: 9 Calculation Patterns covers the companion meter, and 9 Form Validation UI Patterns — As You Type is the closer relative for the error state around the same input.

Nine Password Reveal Patterns, Measured in Area and Intensity

Several of these nine tint or repaint a card-sized backdrop as their state changes, which is why cumulative area sits above 75% for six of the nine below — cumulative area is the share of each fixed-camera frame that actually repainted, averaged across every measured frame, and intensity is the average pixel delta where it did.

| Pattern | Cumulative area | Avg. intensity | Frames moved |

|---|---|---|---|

| 01 Eye icon morph | 76.0% | 44.6 | 14/23 |

| 02 Capslock warning spring | 75.9% | 49.3 | 15/23 |

| 03 Multi-field sync toggle | 76.0% | 65.2 | 10/23 |

| 04 Paste-detect banner | 10.5% | 57.4 | 12/23 |

| 05 Autofill highlight glow | 23.9% | 34.0 | 11/23 |

| 06 Generate-copy cascade | 14.5% | 126.1 | 14/23 |

| 07 Hold-to-show progress | 75.9% | 92.4 | 10/23 |

| 08 Keyboard shortcut focus ring | 75.8% | 29.0 | 10/23 |

| 09 Hero signup form scene | 75.0% | 75.9 | 16/23 |

Where the Value Changes the Reaction — Capslock Badge Overshoot, Autofill Glow Pulse, Generate Cascade Speed

Three numbers below do most of the deciding: CAPSLOCK_BADGE_OVERSHOOT_PX sets how far item 02's badge overshoots past its resting position before the spring settles it back, AUTOFILL_GLOW_PULSE_MS sets how long one sine-times-decay pulse takes to spread and fade around item 05's field, and GENERATE_CASCADE_STEP_MS sets how many milliseconds pass between each letter item 06 reveals. None of the three change what shape draws, only how long the eye spends watching the value move — which is most of what separates a show password icon that feels considered from one that just flips a class.

Where It Breaks — Forced Browser Masking, Missing Screen Reader Announcements, Hold-Gesture Conflicts

A password field's masking is entirely tied to its type attribute — no CSS can un-mask the dots while type stays password — so six of these nine (01, 02, 03, 07, 08, 09) actually flip input.type between password and text, while 04, 05, and 06 demonstrate their technique on a field that was never masked to begin with, since reveal isn't the point being shown. Only three of the nine — 02, 04, and 06 — announce their state change through an aria-live region, so a screen reader user toggling one of the other six has only the button's own aria-pressed value to go on, with no separate spoken confirmation of the change. Item 07's hold gesture guards against its own repeat events with an explicit e.repeat check on keydown — skip that guard and the operating system's own key-repeat would restart startHold() on every repeated event instead of letting one held key fill the ring once, and a pointer dragged off the button mid-hold is caught by pointerleave for the same reason. The nine index.html/style.scss pairs behind these builds, xnzbmd2q, are the fastest way to see exactly which of the nine flips a real type attribute and which only looks like it does.

Three Variants and Accessibility

| Variant | Value change | Feel |

|---|---|---|

| Adjust the caps lock badge overshoot | 02 CAPSLOCK_BADGE_OVERSHOOT_PX 6 → 14 | Lowering the value makes the badge settle with barely a bounce, while raising it sends the badge overshooting much further before it settles back. |

| Adjust the autofill glow pulse duration | 05 AUTOFILL_GLOW_PULSE_MS 900 → 400 | Raising the value makes the glow spread and fade slowly and softly, while lowering it makes the glow flash quickly like a blink. |

| Adjust the generate cascade speed | 06 GENERATE_CASCADE_STEP_MS 60 → 20 | Raising the value makes each letter appear slowly and distinctly one at a time, while lowering it makes the whole string spill out almost at once. |

prefers-reduced-motion is checked at the top of all nine scripts, and instead of turning off individual properties each one jumps straight to its resting value — item 02's badge appears at its final position with no spring pass, and item 06's cascade fills the whole string in one step rather than one letter per GENERATE_CASCADE_STEP_MS. Every toggle here is a real <button type="button">, so aria-pressed mirrors whether the field is currently unmasked on every one of them; Enter and Space activate five of the six — 01, 02, 03, 08, and 09 — through the browser's own button semantics with no hand-rolled keydown handler, while item 07 needs its own keydown/keyup/pointerdown trio instead, since holding down a button isn't something a native click can express. Item 08 adds a second path on top of that: a Ctrl+Shift+H shortcut on the field itself calls the identical toggleReveal() function a click would, then brightens a --ring-alpha custom property so a keyboard-only visitor sees the same field answer that a mouse click would produce. <input type="password">, :autofill, prefers-reduced-motion, and requestAnimationFrame() are documented on MDN.

How to Reveal Password Without Breaking the Eye Icon?

If you've searched how to reveal dotted password without breaking the eye icon, the answer sitting inside the six of these nine builds that actually reveal one — 01, 02, 03, 07, 08, and 09 — is the same: flip type between password and text on the real input element and let a single progress value — not a second hidden input or a CSS trick — drive whatever the icon or field does at the same time. An icon that only changes its own artwork while the field underneath stays masked, or unmasks without the icon noticing, is how the two drift out of sync; each of those six ties the icon's state and the field's type to one shared variable so they can't disagree. The same one-shared-value discipline carries over to adjacent auth UI: OTP Input UI: 9 States, Validation Included ties a similar single state value to per-box masking and validation.

Questions to Answer Before You Build One of These Nine

Does flipping type from password to text count as an accessibility issue by itself?

No — that type change is the standard, accessible way to reveal a password field, and it already reads differently to assistive technology than a masked field does. What each of these nine adds on top of that baseline is a cue for the toggle action itself, since only 02, 04, and 06 use aria-live to announce it — the accessibility gap sits in that missing announcement on the other six, not in changing type.

Which of the nine repaints the most of the frame, and why?

Items 01 and 03 tie at 76.0% cumulative area — both cross-fade an entire overlay of masked dots and real characters across the whole field width on every keystroke rather than animating one small element, so nearly the whole input repaints each time buildOverlay() or paintEyes() runs.

Does the paste-detection banner read what was pasted into the password field?

No — handlePaste() fires on the input's real paste event but never reads event.clipboardData or the field's own value; the banner only confirms that a paste happened, which is enough to explain a password manager's autofill-by-paste behavior without exposing what was pasted.

Enter the archive password

The password is inside this article. You will find it as you read.