GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

비밀번호 표시 아이콘 9가지 — 입력창 속 문자가 도트로 바뀌는 계산

비밀번호 표시 아이콘 밑에는 아홉 갈래로 갈라지는 골목이 있다. 01번 골목은 글자와 점이 자리를 바꾸는 교차로이고, 05번 골목은 빛이 벽을 타고 번지는 통로이며, 06번 골목은 글자가 한 걸음씩 걸어 들어오는 계단이다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

비밀번호 표시 아이콘 9가지 — input type 값 하나가 아홉 가지 방식으로 바뀌는 순간

겉보기엔 눈 모양 아이콘 하나가 켜졌다 꺼졌다 하는 것 같지만, 아홉 항목이 여는 계산은 서로 다르다. input 의 type 값을 password↔text 로 바꾸는 한 줄은 아홉 중 여섯(01·02·03·07·08·09)에 공통이고, 그 한 줄 앞뒤에 스프링을 붙이는지·글자를 한 자씩 순서대로 드러내는지·홀드 시간을 링으로 쌓는지가 항목마다 갈린다. 나머지 셋(04·05·06)은 이미 열려 있는 입력창 위에 붙여넣기 감지·자동입력 글로우·생성 캐스케이드를 얹는다. 아래 아홉 개는 전부 같은 비밀번호 입력창 문제를 각자 다른 방식으로 풀었다.

패턴별 코드

아홉 항목의 핵심 구간만 그대로 옮겼다 — 나머지 스타일·마크업은 데모 아래 압축 파일에 전부 들어 있다.

01글자와 점이 자리를 바꾸는 눈 아이콘 모프

눈 아이콘을 누르면 buildOverlay 가 만든 pem__ch 칸마다 마스킹 점과 실제 문자를 겹쳐 두고, 같은 자리에서 투명도만 반대로 옮겨 교차 페이드로 서로 바뀐다. 글자 순서대로 transitionDelay 를 계단식으로 물려 한 번에 켜지지 않고 왼쪽부터 차례로 넘어가는 것처럼 보인다. 01 항목은 누적면적 76.0%·강도 평균 44.6·23프레임 중 14프레임에서 움직였다.

글자와 점이 자리를 바꾸는 눈 아이콘 모프
새 창에서 열기

01 · 글자와 점이 자리를 바꾸는 눈 아이콘 모프 — 측정 누적면적 76.015% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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);

  input.addEventListener('input', function () {

02캡스락 켜짐을 스프링으로 알리는 경고 배지

onKey 가 KeyboardEvent.getModifierState('CapsLock') 값을 실제로 읽어 setCapsLock 하나로 넘기고, 값이 켜짐으로 바뀌는 순간 stepSpring 이 배지 위치를 목표보다 더 밀어 올렸다가 되돌아오는 오버슈트로 그린다. 같은 호출에서 aria-live 문구도 같이 갱신돼 배지 색만 보지 않아도 상태 전환을 알 수 있다. 02 항목은 누적면적 75.9%·강도 평균 49.3·23프레임 중 15프레임에서 움직였다.

캡스락 켜짐을 스프링으로 알리는 경고 배지
새 창에서 열기

02 · 캡스락 켜짐을 스프링으로 알리는 경고 배지 — 측정 누적면적 75.869% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CAPSLOCK_BADGE_OVERSHOOT_PXstepSpringpcs__badge--active
  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);
    announce.textContent = on ? ANNOUNCE_ON : ANNOUNCE_OFF;
    if (reduce) {
      if (springRaf) { cancelAnimationFrame(springRaf); springRaf = null; }
      pos = target;
      vel = 0;
      lastT = null;
      paintBadge(pos);
      return;
    }
    if (!springRaf) { lastT = null; springRaf = requestAnimationFrame(stepSpring); }
  }

  function onKey(e) {
    if (!e.getModifierState) return;
    setCapsLock(e.getModifierState('CapsLock'));
  }

03여러 입력 필드가 한 번에 맞춰 열리는 동기화 토글

비밀번호와 비밀번호 확인 두 칸이 revealed 값 하나를 같이 보고 있어서, setRevealed 한 번 호출로 inputA·inputB 의 type 이 동시에 같은 상태로 바뀐다. 두 토글 버튼 중 어느 쪽을 눌러도 onToggle 이 같은 함수를 부르기 때문에 두 칸이 따로 노는 순간이 없다. 03 항목은 누적면적 76.0%·강도 평균 65.2·23프레임 중 10프레임에서 움직였다.

여러 입력 필드가 한 번에 맞춰 열리는 동기화 토글
새 창에서 열기

03 · 여러 입력 필드가 한 번에 맞춰 열리는 동기화 토글 — 측정 누적면적 76.029% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SYNC_EYE_MORPH_MSpaintEyespfs__hint
  function setRevealed(next) {
    revealed = next;
    var type = revealed ? 'text' : 'password';
    inputA.type = type;
    inputB.type = type;
    root.classList.toggle('pfs--revealed', revealed);
    var label = revealed ? LABEL_HIDE : LABEL_SHOW;
    toggleA.setAttribute('aria-pressed', String(revealed));
    toggleA.setAttribute('aria-label', label);
    toggleB.setAttribute('aria-pressed', String(revealed));
    toggleB.setAttribute('aria-label', label);
    animateEyeTo(revealed ? 1 : 0);
  }

  function onToggle() {
    root.classList.remove('is-demo');
    setRevealed(!revealed);
  }
  toggleA.addEventListener('click', onToggle);
  toggleB.addEventListener('click', onToggle);

  form.addEventListener('submit', function (e) { e.preventDefault(); });

04붙여넣기를 감지해 밀려나오는 안내 배너

입력창에 건 paste 이벤트가 실제로 발생하면 handlePaste 가 배너에 문구를 채우고 is-visible 클래스를 붙여 슬라이드로 밀어낸 뒤, 일정 시간 뒤 PASTE_BANNER_VISIBLE_MS·PASTE_BANNER_SLIDE_MS 두 값 순서대로 다시 접어 넣는다. 데모 화면의 시연 버튼은 같은 handlePaste 를 흉내로 호출할 뿐 실제 붙여넣기 이벤트는 방문자가 직접 입력창에 붙여넣을 때만 발생한다. 04 항목은 누적면적 10.5%·강도 평균 57.4·23프레임 중 12프레임에서 움직였다.

붙여넣기를 감지해 밀려나오는 안내 배너
새 창에서 열기

04 · 붙여넣기를 감지해 밀려나오는 안내 배너 — 측정 누적면적 10.463% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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(); });

05브라우저 자동입력을 감지해 번지는 하이라이트 글로우

이 항목의 버튼은 이름 그대로 '자동입력 흉내' 라 눌렀을 때 fillAndGlow 가 값을 채우고 glowTick 이 필드 둘레 글로우를 펄스로 번지게 하는데, 브라우저가 실제로 채운 자동입력 상태를 감지하는 API 호출은 없다 — 시연용 버튼이 값 채우기와 글로우 재생을 동시에 흉내 낼 뿐이다. AUTOFILL_GLOW_PULSE_MS 값이 그 펄스 한 번의 길이를 정한다. 05 항목은 누적면적 23.9%·강도 평균 34.0·23프레임 중 11프레임에서 움직였다.

브라우저 자동입력을 감지해 번지는 하이라이트 글로우
새 창에서 열기

05 · 브라우저 자동입력을 감지해 번지는 하이라이트 글로우 — 측정 누적면적 23.938% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

AUTOFILL_GLOW_PULSE_MSfillAndGlowafg__input
  function fillAndGlow() {
    input.value = seedValue;
    if (reduce) {
      input.style.setProperty('--glow', '0');
      return;
    }
    if (glowRaf) cancelAnimationFrame(glowRaf);
    glowStartTs = null;
    glowRaf = requestAnimationFrame(glowTick);
  }

  btn.addEventListener('click', fillAndGlow);

  if (reduce) root.classList.remove('is-demo');
  if (root.classList.contains('is-demo')) {
    setTimeout(function () {
      fillAndGlow();
      setInterval(function () {

06생성 버튼을 누르면 한 글자씩 나타나는 캐스케이드

생성 버튼을 누르면 generatePassword 가 미리 seedRand 로 만들어 둔 후보 문자열 중 다음 것을 골라 cascade 상태를 초기화하고, stepCascade 가 GENERATE_CASCADE_STEP_MS 간격으로 한 글자씩 드러낸 뒤 끝나면 복사 아이콘을 체크 모양으로 모프한다. 이 모프는 opacity 크로스페이드일 뿐 클립보드에 실제로 값을 복사하는 호출은 없다. 06 항목은 누적면적 14.5%·강도 평균 126.1·23프레임 중 14프레임에서 움직였다.

생성 버튼을 누르면 한 글자씩 나타나는 캐스케이드
새 창에서 열기

06 · 생성 버튼을 누르면 한 글자씩 나타나는 캐스케이드 — 측정 누적면적 14.485% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GENERATE_CASCADE_STEP_MSCOPY_MORPH_MSgeneratePassword
  function generatePassword() {
    candIndex = (candIndex + 1) % CANDIDATES.length;
    var target = CANDIDATES[candIndex];
    cascade.target = target;
    cascade.acc = 0;
    cascade.morphAcc = 0;
    root.style.setProperty('--copyMorph', '0');
    root.classList.remove('is-copied');
    if (reduce) {
      cascade.revealed = target.length;
      cascade.active = false;
      cascade.morphing = false;
      display.textContent = target;
      root.style.setProperty('--copyMorph', '1');
      root.classList.add('is-copied');
      return;
    }
    cascade.revealed = 0;
    cascade.active = true;
    cascade.morphing = false;
    display.textContent = '';
  }

  function stepCascade(dtMs) {

07길게 누르는 동안만 진행 링으로 열리는 홀드 방식

버튼을 누르고 있으면 startHold 가 시작 시각을 찍고 매 프레임 loop 가 경과 시간을 HOLD_DURATION_MS 로 나눠 진행 링을 채우다가, 다 차면 문자를 열고 손을 떼는 순간 endHold 가 즉시 다시 마스킹한다. pointerup·pointercancel·pointerleave·키보드 keyup 까지 손을 떼는 경로마다 전부 endHold 를 걸어 뒀지만 blur·focusout 리스너는 없다. 07 항목은 누적면적 75.9%·강도 평균 92.4·23프레임 중 10프레임에서 움직였다.

길게 누르는 동안만 진행 링으로 열리는 홀드 방식
새 창에서 열기

07 · 길게 누르는 동안만 진행 링으로 열리는 홀드 방식 — 측정 누적면적 75.911% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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

  function endHold() {
    if (!holding) return;
    holding = false;
    revealed = false;
    input.type = 'password';
    btn.setAttribute('aria-pressed', 'false');
    setRing(0);
    setEye(0);
    if (rafId) cancelAnimationFrame(rafId);
    rafId = null;
  }

08단축키로 토글하면 초점 링이 밝아지는 키보드 경로

입력창에 건 keydown 이 지정한 단축키 조합을 잡아 toggleReveal 로 type 값을 바꾸는 것과 별개로, 같은 순간 flashFocusRing 이 step 루프를 돌며 --ring-alpha 값을 최대치에서 0으로 서서히 낮춰 초점 링이 반짝였다 가라앉는 것처럼 보이게 한다. 마우스로 눈 아이콘을 눌러도 같은 toggleReveal 을 부르기 때문에 두 경로의 결과 상태는 항상 같다. 08 항목은 누적면적 75.8%·강도 평균 29.0·23프레임 중 10프레임에서 움직였다.

단축키로 토글하면 초점 링이 밝아지는 키보드 경로
새 창에서 열기

08 · 단축키로 토글하면 초점 링이 밝아지는 키보드 경로 — 측정 누적면적 75.756% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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;
    }

09여러 방식을 한 폼에 모은 회원가입 히어로 장면

01 의 아이콘 모프(heroMorphEye)·05 의 자동입력 글로우 흉내(heroSimulateAutofill)·06 의 생성 캐스케이드를 한 회원가입 폼 위에 같은 시드 난수로 배치해, 세 계산이 서로 다른 필드에서 동시에 돌아가도 값이 겹치지 않게 했다. 각 요소는 원래 항목의 상수·함수를 그대로 재사용할 뿐 새 애니메이션을 추가하지 않는다. 09 항목은 누적면적 75.0%·강도 평균 75.9·23프레임 중 16프레임에서 움직였다.

여러 방식을 한 폼에 모은 회원가입 히어로 장면
새 창에서 열기

09 · 여러 방식을 한 폼에 모은 회원가입 히어로 장면 — 측정 누적면적 75.046% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

HERO_CASCADE_STEP_MSheroSimulateAutofillheroMorphEye
  function heroMorphEye(progress) {
    var eased = easeOutCubic(progress);
    var lift = 8 * eased;
    lidTop.setAttribute('d', 'M2,12 Q12,' + (12 - lift) + ' 22,12');
    lidBottom.setAttribute('d', 'M2,12 Q12,' + (12 + lift) + ' 22,12');
    pupil.style.opacity = String(eased);
  }
  heroMorphEye(0);

  var eyeProgress = 0, eyeFrom = 0, eyeTarget = 0, eyeStart = 0, eyeRafId = null;
  function eyeStep(ts) {
    var t = Math.min(1, (ts - eyeStart) / HERO_EYE_MORPH_MS);
    eyeProgress = eyeFrom + (eyeTarget - eyeFrom) * easeOutCubic(t);
    heroMorphEye(eyeProgress);
    if (t < 1) { eyeRafId = requestAnimationFrame(eyeStep); } else { eyeRafId = null; }
  }
  function animateEyeTo(target) {
    eyeFrom = eyeProgress;
    eyeTarget = target;
    eyeStart = performance.now();
    if (eyeRafId) cancelAnimationFrame(eyeRafId);
    eyeRafId = requestAnimationFrame(eyeStep);

값이 반응을 바꾸는 곳 — 캡스락 배지 오버슈트·자동입력 글로우 펄스·생성 캐스케이드 속도

아홉 항목 중 다섯은 눈을 뜨는 순간 카드 배경 톤 자체가 상태에 맞춰 물들도록 짜여 있어서, 실제로 움직이는 부분은 아이콘 하나·링 하나뿐이어도 누적면적 수치는 75%대까지 크게 잡힌다 — 아래 표의 큰 값은 배경 전체가 반응한다는 뜻이지 화면이 어지럽다는 뜻이 아니다.

항목 누적면적 % 강도 평균 움직인 프레임
01 눈 아이콘 모프 76.0% 44.6 14/23
02 캡스락 경고 배지 75.9% 49.3 15/23
03 동기화 토글 76.0% 65.2 10/23
04 붙여넣기 배너 10.5% 57.4 12/23
05 자동입력 글로우 23.9% 34.0 11/23
06 생성 캐스케이드 14.5% 126.1 14/23
07 홀드 진행 링 75.9% 92.4 10/23
08 단축키 초점 링 75.8% 29.0 10/23
09 회원가입 히어로 75.0% 75.9 16/23

이 중 세 값은 느낌 자체를 바꾼다. CAPSLOCK_BADGE_OVERSHOOT_PX 는 캡스락 배지가 튀어 오르는 거리를, AUTOFILL_GLOW_PULSE_MS 는 자동입력 글로우가 한 번 번지는 데 걸리는 시간을, GENERATE_CASCADE_STEP_MS 는 생성 캐스케이드가 한 글자씩 나타나는 간격을 정한다. 세 값은 뒤쪽 변형 세 가지와 접근성에서 실제로 바꿔 본다.

어디서 깨지는가 — 브라우저 강제 마스킹·스크린리더 상태 안내·홀드 제스처 충돌

input 의 type 값을 password 로 두는 것 자체가 플랫폼 표준 동작이라, 이 아홉 항목이 아무리 정교하게 마스킹·모프를 그려도 그 위에 브라우저 자체의 강제 마스킹이 한 겹 더 얹혀 있다 — 표시 상태를 우리 쪽 값 하나로만 관리할 수 없다는 뜻이다. 상태 변화를 소리로 안내하는 aria-live 문구는 02·04·06 세 항목에만 있고 나머지 여섯은 시각 변화만 있어서, 화면을 보지 않는 방식으로 접근하면 캡스락 경고나 붙여넣기 안내를 놓칠 수 있다. 07 은 포인터가 버튼 밖으로 빠지는 경로까지는 pointercancel·pointerleave 로 잡아 두었지만, 키보드 스페이스로 홀드를 시작한 뒤 keyup 없이 다른 창으로 포커스가 넘어가면 blur·focusout 리스너가 없어 홀드 상태가 그대로 남을 수 있다 — 실무에 붙일 때는 이 두 리스너를 추가로 걸어야 한다.

비밀번호 표시 기능을 실무에 붙일 때는 이런 자동완성·붙여넣기 경로까지 같이 신경 써야 틈이 줄어든다. 다운로드에 담긴 압축 파일 비밀번호는 xnzbmd2q 이다.

변형 세 가지와 접근성

항목 상수 값 느낌
02 CAPSLOCK_BADGE_OVERSHOOT_PX 6 → 14 값을 줄이면 배지가 거의 튀지 않고 자리에 앉고, 값을 늘리면 배지가 훨씬 크게 튀어 올랐다가 제자리로 돌아온다
05 AUTOFILL_GLOW_PULSE_MS 900 → 400 값을 늘리면 글로우가 천천히 은은하게 퍼졌다 잦아들고, 값을 줄이면 글로우가 빠르게 깜빡이듯 번쩍인다
06 GENERATE_CASCADE_STEP_MS 60 → 20 값을 늘리면 글자가 한 자씩 또박또박 천천히 나타나고, 값을 줄이면 문자열 전체가 거의 한 번에 쏟아지듯 나타난다

접근성 처리는 항목 소스에 이미 박혀 있는 분기를 그대로 읽은 것이다. prefers-reduced-motion: reduce 분기는 아홉 항목 전부에 있어 동작 줄임 설정을 켠 방문자에게는 스프링·펄스·캐스케이드 없이 목표 상태로 바로 건너뛴다. 눈을 뜨고 닫는 버튼을 가진 항목(01·02·03·07·08·09)은 aria-pressed 로 현재 열림·닫힘 상태를 알리고, 03 의 두 토글 버튼은 같은 상태를 각자의 aria-label 로 다시 읽어 준다. 07 의 홀드 버튼은 키보드에서도 Space 키로 동일하게 눌러 잡을 수 있어 포인터가 없어도 같은 결과를 얻는다.

이 아홉 개를 다른 입력창 부품과 묶는 자리

회원가입 폼처럼 비밀번호 강도까지 같이 보여줘야 하는 화면이라면 비밀번호 강도 UI 9종 — 강도 값이 계산되는 아홉 가지 방식 을 03 이나 09 옆에 붙이면 입력창 상태 표시를 한 세트로 묶을 수 있고, 오류 문구까지 같이 다뤄야 한다면 폼 유효성 검사 UI 9종 — 입력하는 순간 판정하는 오류 안내 쪽 판정 방식과 맞춰 보는 편이 낫다.

비밀번호 표시 버튼은 눈 아이콘과 뭐가 다른가?

눈 아이콘은 화면에 그려지는 그 부품 자체(선 몇 개로 된 아이콘 모양)를 가리키는 말이고, 표시 버튼은 그 부품을 눌렀을 때 type 값이 바뀌는 동작 전체를 가리키는 더 넓은 말이다. 이 글의 아홉 항목은 부품 생김새는 다들 비슷한 눈 모양이어도 값이 바뀌는 계산(스프링·캐스케이드·홀드·단축키)이 서로 달라서, 이 관점에서 보면 아홉 개가 전부 다른 답이다. 여러 필드를 한 폼에서 검증까지 같이 다루는 자리라면 인증번호 입력 UI 만들 때 겪는 9가지 상황 — 폼 검증까지 챙긴 쪽 입력 흐름과 비교해 보는 편이 낫다.

실무에 붙이기 전에 자주 나오는 질문

비밀번호 입력창에 자동입력 감지 기능을 실제로 넣을 수 있나요?

05 는 감지가 아니라 시연용 버튼으로 값 채우기와 글로우를 흉내 낸 것이고, 브라우저가 실제로 채운 자동입력 상태를 감지하는 표준 API는 없어서 :autofill 의사 클래스로 스타일만 반응시키는 우회가 현실적인 선택이다.

이 아홉 개를 하나의 폼에 전부 같이 써도 되나요?

가능은 하지만 09 처럼 세 개까지만 묶는 편이 낫다. requestAnimationFrame 루프가 항목마다 따로 돌기 때문에 아홉 개를 동시에 켜 두면 계산량이 그만큼 늘어난다.

비밀번호 눈 표시 아이콘 대신 이 중 어떤 항목을 먼저 붙이면 되나요?

일반 로그인 폼이라면 01 만으로 충분하고, 캡스락 오타가 잦은 팀이라면 02 를, 회원가입처럼 여러 필드를 한 번에 다루는 화면이라면 03 이나 09 를 먼저 붙이는 편이 맞다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.