09Hero Scene Combining Several Patterns in One Signup Form
Item 01's icon morph, item 05's autofill glow, and item 06's generate cascade all run together on one signup form.
Best forSignup section on a landing page
HERO_CASCADE_STEP_MSheroSimulateAutofillheroMorphEye

<main class="stage">
<div class="phs is-demo" id="phsRoot">
<form class="phs__card" id="phsForm">
<p class="phs__title">Sign up</p>
<div class="phs__row">
<label class="phs__label" for="phsEmail">Email</label>
<div class="phs__staticField" id="phsEmail">[email protected]</div>
</div>
<div class="phs__row">
<label class="phs__label" for="phsPw">Password</label>
<div class="phs__field" id="phsField">
<input class="phs__input" id="phsPw" type="password" aria-describedby="phsHint">
<button class="phs__eyeBtn" id="phsEyeBtn" type="button" aria-pressed="false" aria-label="Show password">
<svg class="phs__eye" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path class="phs__lidTop" id="phsLidTop" d="M2,12 Q12,4 22,12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path class="phs__lidBottom" id="phsLidBottom" d="M2,12 Q12,20 22,12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle class="phs__pupil" id="phsPupil" cx="12" cy="12" r="3.2" fill="currentColor"/>
</svg>
</button>
</div>
</div>
<div class="phs__actions">
<button class="phs__autofillBtn" id="phsAutofillBtn" type="button">Simulate autofill</button>
<button class="phs__generateBtn" id="phsGenerateBtn" type="button">
<span>Generate</span>
<span class="phs__icoWrap" aria-hidden="true">
<svg class="phs__copyIcon" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M216 40v128h-48V88H88V40Z" opacity=".2"/><path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z"/></g></svg>
<span class="phs__checkIcon">
<span class="phs__checkBar phs__checkBar--short"></span>
<span class="phs__checkBar phs__checkBar--long"></span>
</span>
</span>
</button>
</div>
<p class="phs__hint" id="phsHint">The simulate-autofill and generate buttons call the exact same functions a real click would.</p>
</form>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-ink);
.phs {
position: relative;
width: min(92vw, calc(160vh - 12.8vw));
aspect-ratio: 480 / 300;
margin: 0 auto;
border-radius: $r-card;
overflow: hidden;
box-shadow: $shadow-raised;
background: $stage-ink;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: $sp-5;
}
.phs__card {
width: 100%;
max-width: 21em;
display: flex;
flex-direction: column;
gap: $sp-3;
margin: 0;
color: $subject-cream;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.phs__title {
margin: 0;
font-weight: 700;
font-size: 15px;
letter-spacing: .02em;
}
.phs__row {
display: flex;
flex-direction: column;
gap: $sp-1;
}
.phs__label {
font-weight: 600;
font-size: 11px;
color: rgba(255, 247, 230, .64);
}
.phs__staticField {
--glow: 0;
border-radius: $r-control;
padding: $sp-2 $sp-3;
background: rgba(255, 247, 230, .07);
border: 1px solid rgba(255, 247, 230, .16);
color: rgba(255, 247, 230, .8);
font-size: 13px;
box-shadow: 0 0 0 calc(1px + var(--glow) * 3px) rgba(255, 210, 63, calc(.2 + var(--glow) * .6));
}
.phs__field {
--glow: 0;
display: flex;
align-items: center;
gap: $sp-2;
border-radius: $r-control;
padding: $sp-2 $sp-3;
background: rgba(255, 247, 230, .07);
border: 1px solid rgba(255, 247, 230, .16);
box-shadow: 0 0 0 calc(1px + var(--glow) * 3px) rgba(255, 210, 63, calc(.2 + var(--glow) * .6));
}
.phs__input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: $subject-cream;
font-size: 13px;
letter-spacing: .06em;
font-family: inherit;
min-width: 0;
}
.phs__eyeBtn {
display: grid;
place-items: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: $r-pill;
background: transparent;
color: $subject-cream;
cursor: pointer;
flex: none;
}
.phs__eyeBtn:focus-visible {
outline: 2px solid $subject-cream;
outline-offset: 2px;
}
.phs__actions {
display: flex;
gap: $sp-2;
margin-top: $sp-1;
}
.phs__autofillBtn, .phs__generateBtn {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: $sp-1;
border: none;
border-radius: $r-control;
padding: $sp-2 $sp-3;
font-family: inherit;
font-weight: 600;
font-size: 12px;
cursor: pointer;
}
.phs__autofillBtn {
background: rgba(255, 247, 230, .1);
color: $subject-cream;
}
.phs__generateBtn {
background: $subject-cream;
color: $subject-ink;
}
.phs__autofillBtn:focus-visible, .phs__generateBtn:focus-visible {
outline: 2px solid $subject-cream;
outline-offset: 2px;
}
.phs__icoWrap {
position: relative;
display: inline-grid;
place-items: center;
width: 14px;
height: 14px;
}
.phs__copyIcon {
width: 14px;
height: 14px;
opacity: 1;
transition: opacity $dur-quick $ease-out;
}
.phs__checkIcon {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity $dur-quick $ease-out;
}
.phs__checkBar {
position: absolute;
left: 1px;
top: 7px;
height: 2px;
background: currentColor;
border-radius: $r-xs;
transform-origin: left center;
}
.phs__checkBar--short { width: 5px; transform: rotate(225deg); }
.phs__checkBar--long { width: 10px; transform: rotate(-48deg); }
.phs.is-generated .phs__copyIcon { opacity: 0; }
.phs.is-generated .phs__checkIcon { opacity: 1; }
// 배경 톤: 필드 안쪽 골드 glow 와 같은 톤을 is-generated 순간 카드 전체로 넓힌다(새 색 없음).
.phs::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: rgba(255, 210, 63, 0);
transition: background-color .3s ease;
}
.phs.is-generated::before {
background: rgba(255, 210, 63, .1);
}
.phs__hint {
margin: $sp-1 0 0;
font-size: 11px;
line-height: 1.4;
color: rgba(255, 247, 230, .56);
}
@media (max-width: 340px) {
.phs { padding: $sp-3; }
.phs__card { gap: $sp-2; }
.phs__hint { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.phs__copyIcon, .phs__checkIcon { transition: none; }
.phs::before { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.phs {
position: relative;
width: min(92vw, 160vh - 12.8vw);
aspect-ratio: 480/300;
margin: 0 auto;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
background: #17141a;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 20px;
}
.phs__card {
width: 100%;
max-width: 21em;
display: flex;
flex-direction: column;
gap: 12px;
margin: 0;
color: #fff7e6;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.phs__title {
margin: 0;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.02em;
}
.phs__row {
display: flex;
flex-direction: column;
gap: 4px;
}
.phs__label {
font-weight: 600;
font-size: 11px;
color: rgba(255, 247, 230, 0.64);
}
.phs__staticField {
--glow: 0;
border-radius: 12px;
padding: 8px 12px;
background: rgba(255, 247, 230, 0.07);
border: 1px solid rgba(255, 247, 230, 0.16);
color: rgba(255, 247, 230, 0.8);
font-size: 13px;
box-shadow: 0 0 0 calc(1px + var(--glow) * 3px) rgba(255, 210, 63, calc(0.2 + var(--glow) * 0.6));
}
.phs__field {
--glow: 0;
display: flex;
align-items: center;
gap: 8px;
border-radius: 12px;
padding: 8px 12px;
background: rgba(255, 247, 230, 0.07);
border: 1px solid rgba(255, 247, 230, 0.16);
box-shadow: 0 0 0 calc(1px + var(--glow) * 3px) rgba(255, 210, 63, calc(0.2 + var(--glow) * 0.6));
}
.phs__input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: #fff7e6;
font-size: 13px;
letter-spacing: 0.06em;
font-family: inherit;
min-width: 0;
}
.phs__eyeBtn {
display: grid;
place-items: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: 999px;
background: transparent;
color: #fff7e6;
cursor: pointer;
flex: none;
}
.phs__eyeBtn:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.phs__actions {
display: flex;
gap: 8px;
margin-top: 4px;
}
.phs__autofillBtn, .phs__generateBtn {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
border: none;
border-radius: 12px;
padding: 8px 12px;
font-family: inherit;
font-weight: 600;
font-size: 12px;
cursor: pointer;
}
.phs__autofillBtn {
background: rgba(255, 247, 230, 0.1);
color: #fff7e6;
}
.phs__generateBtn {
background: #fff7e6;
color: #17141a;
}
.phs__autofillBtn:focus-visible, .phs__generateBtn:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.phs__icoWrap {
position: relative;
display: inline-grid;
place-items: center;
width: 14px;
height: 14px;
}
.phs__copyIcon {
width: 14px;
height: 14px;
opacity: 1;
transition: opacity 220ms ease-out;
}
.phs__checkIcon {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 220ms ease-out;
}
.phs__checkBar {
position: absolute;
left: 1px;
top: 7px;
height: 2px;
background: currentColor;
border-radius: 4px;
transform-origin: left center;
}
.phs__checkBar--short {
width: 5px;
transform: rotate(225deg);
}
.phs__checkBar--long {
width: 10px;
transform: rotate(-48deg);
}
.phs.is-generated .phs__copyIcon {
opacity: 0;
}
.phs.is-generated .phs__checkIcon {
opacity: 1;
}
.phs::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: rgba(255, 210, 63, 0);
transition: background-color 0.3s ease;
}
.phs.is-generated::before {
background: rgba(255, 210, 63, 0.1);
}
.phs__hint {
margin: 4px 0 0;
font-size: 11px;
line-height: 1.4;
color: rgba(255, 247, 230, 0.56);
}
@media (max-width: 340px) {
.phs {
padding: 12px;
}
.phs__card {
gap: 8px;
}
.phs__hint {
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.phs__copyIcon, .phs__checkIcon {
transition: none;
}
.phs::before {
transition: none;
}
}(function () {
// 기법: 눈 모프·글로우 펄스·Generate 캐스케이드 세 계산을 이 파일 안에서 독립적으로 다시 구현한다
// (01/05/06 과 이름이 겹치지 않도록 hero* 접두 함수·HERO_* 상수로 새로 짓는다). 눈은 progress
// 를 목표까지 rAF 로 보간해 베지어 ctrlY 를 다시 계산하고, 글로우는 sin(경과비*PI)*exp(-경과비*k)
// 엔벌로프로 자동입력 하이라이트를 표현하며, 생성은 누적 타이머로 글자를 한 자씩 채운 뒤
// 복사 아이콘을 체크로 크로스페이드한다.
var root = document.getElementById('phsRoot');
var form = document.getElementById('phsForm');
var emailField = document.getElementById('phsEmail');
var pwField = document.getElementById('phsField');
var pwInput = document.getElementById('phsPw');
var eyeBtn = document.getElementById('phsEyeBtn');
var lidTop = document.getElementById('phsLidTop');
var lidBottom = document.getElementById('phsLidBottom');
var pupil = document.getElementById('phsPupil');
var autofillBtn = document.getElementById('phsAutofillBtn');
var generateBtn = document.getElementById('phsGenerateBtn');
var HERO_EYE_MORPH_MS = 260;
var HERO_GLOW_PULSE_MS = 900;
var HERO_CASCADE_STEP_MS = 70;
function seedRand(seed) {
var s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var pwRand = seedRand(426091);
var cascadeRand = seedRand(426092);
var PW_POOL = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%';
function pickPassword(len, rand) {
var out = '';
for (var i = 0; i < len; i += 1) out += PW_POOL[Math.floor(rand() * PW_POOL.length)];
return out;
}
pwInput.value = pickPassword(9, pwRand);
function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
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);
}
function toggleEye() {
var willShow = pwInput.type === 'password';
pwInput.type = willShow ? 'text' : 'password';
eyeBtn.setAttribute('aria-pressed', String(willShow));
animateEyeTo(willShow ? 1 : 0);
}
var glowStart = 0, glowRafId = null;
function setGlow(v) {
emailField.style.setProperty('--glow', String(v));
pwField.style.setProperty('--glow', String(v));
}
function glowStep(ts) {
var frac = (ts - glowStart) / HERO_GLOW_PULSE_MS;
if (frac >= 1) { setGlow(0); glowRafId = null; return; }
var envelope = Math.max(0, Math.sin(frac * Math.PI) * Math.exp(-frac * 2.2));
setGlow(envelope);
glowRafId = requestAnimationFrame(glowStep);
}
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 () {
root.classList.remove('is-generated');
}, 500);
}
}
step();
}
eyeBtn.addEventListener('click', function () { toggleEye(); });
autofillBtn.addEventListener('click', function () { heroSimulateAutofill(); });
generateBtn.addEventListener('click', function () { heroRunCascade(); });
form.addEventListener('submit', function (e) { e.preventDefault(); });
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) root.classList.remove('is-demo');
var demoTimers = [];
function clearDemoTimers() {
for (var i = 0; i < demoTimers.length; i += 1) window.clearTimeout(demoTimers[i]);
demoTimers = [];
}
function demoLoop() {
if (!root.classList.contains('is-demo')) return;
clearDemoTimers();
demoTimers.push(window.setTimeout(function () { toggleEye(); }, 150));
demoTimers.push(window.setTimeout(function () { toggleEye(); }, 650));
demoTimers.push(window.setTimeout(function () { heroSimulateAutofill(); }, 900));
demoTimers.push(window.setTimeout(function () { heroRunCascade(); }, 1500));
demoTimers.push(window.setTimeout(function () {
pwInput.type = 'password';
eyeBtn.setAttribute('aria-pressed', 'false');
animateEyeTo(0);
}, 2600));
demoTimers.push(window.setTimeout(demoLoop, 2800));
}
if (!reduce) demoTimers.push(window.setTimeout(demoLoop, 200));
})();import { useEffect, useRef } from "react";
import styles from "./PasswordRevealHeroSignupFormScene.module.scss";
export default function PasswordRevealHeroSignupFormScene() {
const rootRef = useRef<HTMLDivElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const emailFieldRef = useRef<HTMLDivElement>(null);
const pwFieldRef = useRef<HTMLDivElement>(null);
const pwInputRef = useRef<HTMLInputElement>(null);
const eyeBtnRef = useRef<HTMLButtonElement>(null);
const lidTopRef = useRef<SVGPathElement>(null);
const lidBottomRef = useRef<SVGPathElement>(null);
const pupilRef = useRef<SVGCircleElement>(null);
const autofillBtnRef = useRef<HTMLButtonElement>(null);
const generateBtnRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const root = rootRef.current;
const form = formRef.current;
const emailField = emailFieldRef.current;
const pwField = pwFieldRef.current;
const pwInput = pwInputRef.current;
const eyeBtn = eyeBtnRef.current;
const lidTop = lidTopRef.current;
const lidBottom = lidBottomRef.current;
const pupil = pupilRef.current;
const autofillBtn = autofillBtnRef.current;
const generateBtn = generateBtnRef.current;
if (!root || !form || !emailField || !pwField || !pwInput || !eyeBtn || !lidTop || !lidBottom || !pupil || !autofillBtn || !generateBtn) return;
const HERO_EYE_MORPH_MS = 260;
const HERO_GLOW_PULSE_MS = 900;
const HERO_CASCADE_STEP_MS = 70;
function seedRand(seed: number) {
let s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
const pwRand = seedRand(426093);
const cascadeRand = seedRand(426094);
const PW_POOL = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%";
function pickPassword(len: number, rand: () => number) {
let out = "";
for (let i = 0; i < len; i += 1) out += PW_POOL[Math.floor(rand() * PW_POOL.length)];
return out;
}
pwInput.value = pickPassword(9, pwRand);
function easeOutCubic(t: number) { return 1 - Math.pow(1 - t, 3); }
function heroMorphEye(progress: number) {
const eased = easeOutCubic(progress);
const 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);
let eyeProgress = 0, eyeFrom = 0, eyeTarget = 0, eyeStart = 0, eyeRafId = 0;
function eyeStep(ts: number) {
const t = Math.min(1, (ts - eyeStart) / HERO_EYE_MORPH_MS);
eyeProgress = eyeFrom + (eyeTarget - eyeFrom) * easeOutCubic(t);
heroMorphEye(eyeProgress);
if (t < 1) { eyeRafId = requestAnimationFrame(eyeStep); }
}
function animateEyeTo(target: number) {
eyeFrom = eyeProgress;
eyeTarget = target;
eyeStart = performance.now();
cancelAnimationFrame(eyeRafId);
eyeRafId = requestAnimationFrame(eyeStep);
}
function toggleEye() {
const willShow = pwInput!.type === "password";
pwInput!.type = willShow ? "text" : "password";
eyeBtn!.setAttribute("aria-pressed", String(willShow));
animateEyeTo(willShow ? 1 : 0);
}
let glowStart = 0, glowRafId = 0, glowing = false;
function setGlow(v: number) {
emailField!.style.setProperty("--glow", String(v));
pwField!.style.setProperty("--glow", String(v));
}
function glowStep(ts: number) {
const frac = (ts - glowStart) / HERO_GLOW_PULSE_MS;
if (frac >= 1) { setGlow(0); glowing = false; return; }
const envelope = Math.max(0, Math.sin(frac * Math.PI) * Math.exp(-frac * 2.2));
setGlow(envelope);
glowRafId = requestAnimationFrame(glowStep);
}
function heroSimulateAutofill() {
glowStart = performance.now();
if (glowing) return;
glowing = true;
glowRafId = requestAnimationFrame(glowStep);
}
let cascadeTimer = 0;
function heroRunCascade() {
const target = pickPassword(9, cascadeRand);
let 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 () {
root!.classList.remove("is-generated");
}, 500);
}
}
step();
}
function onEyeClick() { toggleEye(); }
function onAutofillClick() { heroSimulateAutofill(); }
function onGenerateClick() { heroRunCascade(); }
function onSubmit(e: Event) { e.preventDefault(); }
eyeBtn.addEventListener("click", onEyeClick);
autofillBtn.addEventListener("click", onAutofillClick);
generateBtn.addEventListener("click", onGenerateClick);
form.addEventListener("submit", onSubmit);
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) root.classList.remove("is-demo");
let demoTimers: number[] = [];
function clearDemoTimers() {
for (let i = 0; i < demoTimers.length; i += 1) window.clearTimeout(demoTimers[i]);
demoTimers = [];
}
function demoLoop() {
if (!root!.classList.contains("is-demo")) return;
clearDemoTimers();
demoTimers.push(window.setTimeout(function () { toggleEye(); }, 150));
demoTimers.push(window.setTimeout(function () { toggleEye(); }, 650));
demoTimers.push(window.setTimeout(function () { heroSimulateAutofill(); }, 900));
demoTimers.push(window.setTimeout(function () { heroRunCascade(); }, 1500));
demoTimers.push(window.setTimeout(function () {
pwInput!.type = "password";
eyeBtn!.setAttribute("aria-pressed", "false");
animateEyeTo(0);
}, 2600));
demoTimers.push(window.setTimeout(demoLoop, 2800));
}
if (!reduce) demoTimers.push(window.setTimeout(demoLoop, 200));
return () => {
eyeBtn.removeEventListener("click", onEyeClick);
autofillBtn.removeEventListener("click", onAutofillClick);
generateBtn.removeEventListener("click", onGenerateClick);
form.removeEventListener("submit", onSubmit);
cancelAnimationFrame(eyeRafId);
cancelAnimationFrame(glowRafId);
window.clearTimeout(cascadeTimer);
clearDemoTimers();
};
}, []);
return (
<main className={styles.stage}>
<div className={`${styles.phs} is-demo`} ref={rootRef}>
<form className={styles.phs__card} ref={formRef}>
<p className={styles.phs__title}>회원가입</p>
<div className={styles.phs__row}>
<label className={styles.phs__label} htmlFor="phsEmailReact">이메일</label>
<div className={styles.phs__staticField} id="phsEmailReact" ref={emailFieldRef}>[email protected]</div>
</div>
<div className={styles.phs__row}>
<label className={styles.phs__label} htmlFor="phsPwReact">비밀번호</label>
<div className={styles.phs__field} ref={pwFieldRef}>
<input className={styles.phs__input} id="phsPwReact" type="password" aria-describedby="phsHintReact" ref={pwInputRef} readOnly />
<button className={styles.phs__eyeBtn} type="button" aria-pressed="false" aria-label="비밀번호 보기" ref={eyeBtnRef}>
<svg className={styles.phs__eye} viewBox="0 0 24 24" width={18} height={18} aria-hidden="true">
<path className={styles.phs__lidTop} ref={lidTopRef} d="M2,12 Q12,4 22,12" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" />
<path className={styles.phs__lidBottom} ref={lidBottomRef} d="M2,12 Q12,20 22,12" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" />
<circle className={styles.phs__pupil} ref={pupilRef} cx={12} cy={12} r={3.2} fill="currentColor" />
</svg>
</button>
</div>
</div>
<div className={styles.phs__actions}>
<button className={styles.phs__autofillBtn} type="button" ref={autofillBtnRef}>자동입력 흉내</button>
<button className={styles.phs__generateBtn} type="button" ref={generateBtnRef}>
<span>생성</span>
<span className={styles.phs__icoWrap} aria-hidden="true">
<svg className={styles.phs__copyIcon} width="1em" height="1em" viewBox="0 0 256 256">
<g fill="currentColor">
<path d="M216 40v128h-48V88H88V40Z" opacity=".2" />
<path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" />
</g>
</svg>
<span className={styles.phs__checkIcon}>
<span className={`${styles.phs__checkBar} ${styles["phs__checkBar--short"]}`} />
<span className={`${styles.phs__checkBar} ${styles["phs__checkBar--long"]}`} />
</span>
</span>
</button>
</div>
<p className={styles.phs__hint} id="phsHintReact">자동입력 흉내와 생성 버튼은 실제 클릭이 부르는 함수를 그대로 다시 부른다</p>
</form>
</div>
</main>
);
}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 · Show Password Toggle Icon: 9 Reveal Patterns
The post this demo lives in
Show Password Toggle Icon: 9 Reveal PatternsNine show password toggle icon builds: One eye icon opens onto nine forking alleys. Alley 01 crosses letters into dots, See all demos in this post
If you like this effect - similar demos
Auth UI07 App password shown exactly once9 account security settings UI patterns
Auth UI02 Signup steps — a stepped form that divides its own progress ratio to set bar length and color9 Login UI Design Patterns (Copy-Paste)
Auth UI03 Floating-label signup input9 Login Form Design Patterns You Can Copy-Paste