<form action="">
  <div class="form-group">
    <label for="password">Password</label>
    <input id="password" type="password" required />
    <button type="button" title="Reveal Password" aria-pressed="false">
      <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <mask id="eye-open">
            <path d="M1 12C1 12 5 4 12 4C19 4 23 12 23 12V20H12H1V12Z" fill="#D9D9D9" stroke="black" stroke-width="1.5" stroke-linejoin="round" />
          </mask>
          <mask id="eye-closed">
            <path d="M1 12C1 12 5 20 12 20C19 20 23 12 23 12V20H12H1V12Z" fill="#D9D9D9" />
          </mask>
        </defs>
        <path class="lid lid--upper" d="M1 12C1 12 5 4 12 4C19 4 23 12 23 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
        <path class="lid lid--lower" d="M1 12C1 12 5 20 12 20C19 20 23 12 23 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
        <g mask="url(#eye-open)">
          <g class="eye">
            <circle cy="12" cx="12" r="4" fill="currentColor" />
            <circle cy="11" cx="13" r="1" fill="black" />
          </g>
        </g>
      </svg>
      <span class="sr-only">Reveal</span>
    </button>
  </div>
</form>
@font-face {
  font-family: "Geist Mono";
  src: url("https://assets.codepen.io/605876/GeistMonoVariableVF.ttf") format("truetype");
}

:root {
	--grid-offset: calc(50% + 80px);
	--color: hsl(0 0% 6%);
	--bg: hsl(0 0% 96%);
	--color-alpha: hsl(0 0% 60%);
	--selection: hsl(0 0% 80%);
	--bg-size: 180px;
	--grid-line: hsl(0 0% 80%);
	--input-bg: hsl(0 0% 100% / 0.2);
	--grid-accent: hsl(280 0% 10% / 0.1);
	--glint: white;
	--button-shade: 80%;
}
:root:focus-within {
	--grid-accent: hsl(280 0% 10% / 0.35);
}

@media(prefers-color-scheme: dark) {
	:root {
		--button-shade: 30%;
		--glint: black;
		--grid-accent: hsl(280 0% 80% / 0.1);
		--selection: hsl(0 0% 20%);
		--color: hsl(0 0% 98%);
		--bg: hsl(0 0% 6%);
		--color-alpha: hsl(0 0% 50%);
		--grid-line: hsl(0 0% 12%);
		--input-bg: hsl(0 0% 0% / 0.2);
	}
	:root:focus-within {
		--grid-accent: hsl(280 0% 80% / 0.35);
	}
}

*,
*:after,
*:before {
	box-sizing: border-box;
}


body {
	display: grid;
	place-items: center;
	min-height: 100vh;
	font-family:  'Geist Mono', sans-serif, system-ui;
	color: var(--color);
	background: var(--bg);
/*	background: black;*/
}

body::before {
	content: "";
	transition: background 0.2s;
	background:
		/*	How to create one square */
		linear-gradient(var(--grid-accent) 0 2px, transparent 2px calc(100% - 2px), var(--grid-accent) calc(100% - 2px)) calc((var(--grid-offset) - (var(--bg-size) * 2)) - 1px) calc((var(--grid-offset) - var(--bg-size)) - 1px) / calc(var(--bg-size) + 2px) calc(var(--bg-size) + 2px) no-repeat,
		linear-gradient(90deg, var(--grid-accent) 0 2px, transparent 2px calc(100% - 2px), var(--grid-accent) calc(100% - 2px)) calc((var(--grid-offset) - (var(--bg-size) * 2)) - 1px) calc((var(--grid-offset) - var(--bg-size)) - 1px) / calc(var(--bg-size) + 2px) calc(var(--bg-size) + 2px) no-repeat,
		linear-gradient(transparent calc(var(--bg-size) - 2px), var(--grid-line) calc(var(--bg-size) - 2px) var(--bg-size)) var(--grid-offset) var(--grid-offset) / 100% var(--bg-size),
		linear-gradient(90deg, transparent calc(var(--bg-size) - 2px), var(--grid-line) calc(var(--bg-size) - 2px) var(--bg-size)) var(--grid-offset) var(--grid-offset) / var(--bg-size) 100%, transparent;
/*	background: var(--bg);*/
	position: fixed;
	inset: 0;
	height: 100vh;
	width: 100vw;
	-webkit-mask: radial-gradient(circle at 0% 0%, hsl(0 0% 100% / 0.5), transparent);
}

.form-group:focus-within label {
	color: var(--color);
}
.form-group:focus-within input {
	border-color: var(--color);
	color: var(--color);
}
.form-group:focus-within button {
	color: var(--color);
}

input {
	font-family: "Geist Mono", monospace;
	font-size: 1.75rem;
	padding: 1rem 2rem;
  padding-right: 4rem;
	letter-spacing: 0.2ch;
	border-radius: 6px;
	color: var(--color-alpha);
	border-color: var(--color-alpha);
	border-style: solid;
	background: var(--input-bg);
	outline: none;
	transition: border-color 0.2s, color 0.2s
}

label {
	position: absolute;
	color: var(--color-alpha);
	bottom: calc(100% + 0.5rem);
	letter-spacing: 0.2ch;
	transition: color 0.2s;
}

.form-group {
	position: relative;
}

.eye circle:nth-of-type(2) {
	fill: var(--glint);
}

button {
	padding: 0;
	display: grid;
	place-items: center;
	height: 100%;
	aspect-ratio: 1;
	border-radius: 12px;
	border: 0;
	background: linear-gradient(hsl(0 0% var(--button-shade) / calc(var(--active, 0) * 0.5)), hsl(0 0% var(--button-shade) / calc(var(--active, 0) * 0.5))) padding-box;
	border: 6px solid transparent;
	transition: background 0.125s;
	color: var(--color-alpha);
	position: absolute;
	right: 0;
	z-index: 2;
	top: 50%;
	cursor: pointer;
	translate: 0 -50%;
	outline: 0;
}

input::selection {
	background: var(--selection);
}

button:is(:focus-visible, :hover) {
	--active: 1;
}

button svg {
	width: 75%;
}

.sr-only {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border-width: 0;
}
import { gsap } from 'https://cdn.skypack.dev/gsap@3.11.0'  

gsap.registerPlugin(ScrambleTextPlugin, MorphSVGPlugin)

const BLINK_SPEED = 0.075
const TOGGLE_SPEED = 0.125
const ENCRYPT_SPEED = 1

let busy = false

const EYE = document.querySelector('.eye')
const TOGGLE = document.querySelector('button')
const INPUT = document.querySelector('#password')
const PROXY = document.createElement('div')

// 'upperAndLowerCase'
const chars =
	'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`~,.<>?/;":][}{+_)(*&^%$#@!±=-§'

let blinkTl
const BLINK = () => {
  const delay = gsap.utils.random(2, 8)
  const duration = BLINK_SPEED
  const repeat = Math.random() > 0.5 ? 3 : 1
  blinkTl = gsap.timeline({
  	delay,
  	onComplete: () => BLINK(),
  	repeat,
  	yoyo: true,
  })
	  .to('.lid--upper', {
	  	morphSVG: '.lid--lower',
  		duration,
	  })
	  .to('#eye-open path', {
	  	morphSVG: '#eye-closed path',
  		duration,
	  }, 0)
}

BLINK()

const posMapper = gsap.utils.mapRange(-100, 100, 30, -30)
let reset
const MOVE_EYE = ({ x, y }) => {
	if (reset) reset.kill()
	reset = gsap.delayedCall(2, () => {
		gsap.to('.eye', { xPercent: 0, yPercent: 0, duration: 0.2 })
	})
	const BOUNDS = EYE.getBoundingClientRect()
	// Get distance and angle between two points
	gsap.set('.eye', {
		xPercent: gsap.utils.clamp(-30, 30, posMapper(BOUNDS.x - x)),
		yPercent: gsap.utils.clamp(-30, 30, posMapper(BOUNDS.y - y))
	})
}

window.addEventListener('pointermove', MOVE_EYE)

// Trick is to animate from discs and to discs.

TOGGLE.addEventListener('click', () => {
	if (busy) return
	const isText = INPUT.matches('[type=password]')
	const val = INPUT.value
	busy = true
	// Need to stop the blink here and kill it off. 
	TOGGLE.setAttribute('aria-pressed', isText)
	const duration = TOGGLE_SPEED

	if (isText) {
		if (blinkTl) blinkTl.kill()

		gsap.timeline({
			onComplete: () => {
				busy = false
			}
		})
			// Close the eye first and kill the TL
			.to('.lid--upper', {
		  	morphSVG: '.lid--lower',
	  		duration,
		  })
		  .to('#eye-open path', {
		  	morphSVG: '#eye-closed path',
	  		duration,
		  }, 0)
		  // Decrypt the text input
			.to(PROXY, {
				duration: ENCRYPT_SPEED,
				onStart: () => {
					INPUT.type = 'text'
				},
				onComplete: () => {
					PROXY.innerHTML = ''
					INPUT.value = val
				},
				scrambleText: {
					chars,
					text:
						INPUT.value.charAt(INPUT.value.length - 1) === ' '
							? `${INPUT.value.slice(0, INPUT.value.length - 1)}${chars.charAt(
									Math.floor(Math.random() * chars.length)
							  )}`
							: INPUT.value,
				},
				onUpdate: () => {
					const len = val.length - PROXY.innerText.length
					INPUT.value = `${PROXY.innerText}${new Array(len).fill('•').join('')}`
				},
			}, 0)
	} else {
		gsap.timeline({
			onComplete: () => {
				BLINK()
				busy = false
			}
		})
			.to('.lid--upper', {
		  	morphSVG: '.lid--upper',
	  		duration,
		  })
		  .to('#eye-open path', {
		  	morphSVG: '#eye-open path',
	  		duration,
		  }, 0)
			.to(PROXY, {
				duration: ENCRYPT_SPEED,
				onComplete: () => {
					INPUT.type = 'password'
					INPUT.value = val
					PROXY.innerHTML = ''
				},
				scrambleText: {
					chars,
					text: new Array(INPUT.value.length).fill('•').join(''),
				},
				onUpdate: () => {
					INPUT.value = `${PROXY.innerText}${val.slice(
						PROXY.innerText.length,
						val.length
					)}`
				},
			}, 0)
	}
})


const FORM = document.querySelector('form')
FORM.addEventListener('submit', event => event.preventDefault())
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://assets.codepen.io/16327/MorphSVGPlugin3.min.js
  2. https://assets.codepen.io/16327/ScrambleTextPlugin3.min.js