01조건이 차야 풀리는 다음 단계 잠금
필수 조건 세 줄이 하나씩 채워지는 동안 「다음」 버튼은 aria-disabled 로 잠겨 있고, 마지막 줄이 채워지는 순간 잠금이 한 프레임에 풀리며 버튼이 제 색을 찾습니다.
쓰임약관 동의를 다 받아야 넘어가는 가입 단계, 필수 항목이 남은 신청서
aria-disabledsteps(1, end)translateY

<main class="stage">
<section class="lg is-demo" id="lg" aria-label="가입 필수 확인 단계">
<p class="lg__h">2단계 · 필수 확인</p>
<ul class="lg__list">
<li class="lg__row" data-row="1">
<button class="lg__box" type="button" role="checkbox" aria-checked="false">
<span class="lg__tick" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg></span>
<span class="lg__label">이용약관 동의</span>
</button>
</li>
<li class="lg__row" data-row="2">
<button class="lg__box" type="button" role="checkbox" aria-checked="false">
<span class="lg__tick" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg></span>
<span class="lg__label">개인정보 수집 동의</span>
</button>
</li>
<li class="lg__row" data-row="3">
<button class="lg__box" type="button" role="checkbox" aria-checked="false">
<span class="lg__tick" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg></span>
<span class="lg__label">만 14세 이상입니다</span>
</button>
</li>
</ul>
<div class="lg__foot">
<p class="lg__why">
<span class="lg__why-a">남은 필수 항목이 있습니다</span>
<span class="lg__why-b">다음 단계로 넘어갈 수 있습니다</span>
</p>
<button class="lg__next" type="button" id="lg-next" aria-disabled="true">
<span class="lg__lock" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M216 96v112a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8V96a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8" opacity=".2"/><path d="M208 80h-32V56a48 48 0 0 0-96 0v24H48a16 16 0 0 0-16 16v112a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16M96 56a32 32 0 0 1 64 0v24H96Zm112 152H48V96h160z"/></g></svg></span>
<span class="lg__next-t">다음</span>
</button>
</div>
</section>
</main>@use "sass:color";
@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
$line: color.mix($subject-cream, $stage-ink, 16%);
$soft: color.mix($subject-cream, $stage-ink, 8%);
.lg {
box-sizing: border-box;
width: min(304px, 92vw);
padding: $sp-3;
border: 1px solid $line;
border-radius: $r-card;
background: $stage-ink;
color: $subject-cream;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.3;
}
.lg__h {
margin: 0 0 $sp-2;
color: rgba(255, 255, 255, .56);
font-size: 11px;
font-weight: 700;
letter-spacing: .04em;
}
.lg__list {
display: grid;
gap: $sp-1;
margin: 0;
padding: 0;
list-style: none;
}
// ── 조건 한 줄 ────────────────────────────────────
.lg__box {
display: flex;
align-items: center;
gap: $sp-2;
width: 100%;
margin: 0;
padding: $sp-1 $sp-2;
border: 1px solid $line;
border-radius: $r-sm;
background: $soft;
color: inherit;
font-family: inherit;
font-size: 12px;
line-height: 1.3;
text-align: left;
cursor: pointer;
}
.lg__box:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.lg__tick {
display: inline-flex;
flex: 0 0 auto;
width: 16px;
height: 16px;
border-radius: $r-xs;
background: rgba(255, 255, 255, .1);
color: transparent;
}
.lg__label { color: rgba(255, 255, 255, .74); }
// 정적(사람이 조작하는) 경로 — .is-demo 를 떼도 결과가 보여야 한다
.lg__row.is-on .lg__tick { background: $color; color: $subject-cream; }
.lg__row.is-on .lg__label { color: $subject-cream; }
// ── 발판 ─────────────────────────────────────────
.lg__foot {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: $sp-2;
margin-top: $sp-2;
}
.lg__why {
position: relative;
margin: 0;
min-height: 16px;
font-size: 11px;
}
.lg__why-a, .lg__why-b { position: absolute; inset: 0; }
.lg__why-a { color: rgba(255, 255, 255, .5); }
.lg__why-b { color: $subject-mint; opacity: 0; }
.lg.is-ready .lg__why-a { opacity: 0; }
.lg.is-ready .lg__why-b { opacity: 1; }
.lg__next {
display: inline-flex;
align-items: center;
gap: $sp-1;
margin: 0;
padding: $sp-1 $sp-3;
border: 0;
border-radius: $r-pill;
background: rgba(255, 255, 255, .12);
color: rgba(255, 255, 255, .72);
font-family: inherit;
font-size: 12px;
font-weight: 700;
line-height: 1.6;
cursor: not-allowed;
}
.lg__next:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.lg__lock { display: inline-flex; width: 13px; height: 13px; }
.lg.is-ready .lg__next { background: $color; color: $subject-cream; cursor: pointer; }
.lg.is-ready .lg__lock { display: none; }
.lg.is-sent .lg__next-t { color: $subject-mint; }
.lg.is-nudge .lg__why-a { color: $error-soft; }
// ── 격자 미리보기 전용 2초 루프 ─────────────────────
// 켜짐은 steps(1, end) 로 한 프레임에 자르고(반투명 유령 금지),
// 프레임 수는 같은 줄의 translateY 팝이 채운다
@keyframes lg-on-1 {
0%, 11% { background: rgba(255, 255, 255, .1); color: transparent; }
12%, 100% { background: $color; color: $subject-cream; }
}
@keyframes lg-on-2 {
0%, 31% { background: rgba(255, 255, 255, .1); color: transparent; }
32%, 100% { background: $color; color: $subject-cream; }
}
@keyframes lg-on-3 {
0%, 51% { background: rgba(255, 255, 255, .1); color: transparent; }
52%, 88% { background: $color; color: $subject-cream; }
89%, 100% { background: rgba(255, 255, 255, .1); color: transparent; }
}
@keyframes lg-pop-1 {
0%, 8% { transform: translateY(0); }
14% { transform: translateY(-3px); }
22%, 100% { transform: translateY(0); }
}
@keyframes lg-pop-2 {
0%, 28% { transform: translateY(0); }
34% { transform: translateY(-3px); }
42%, 100% { transform: translateY(0); }
}
@keyframes lg-pop-3 {
0%, 48% { transform: translateY(0); }
54% { transform: translateY(-3px); }
62%, 88% { transform: translateY(0); }
100% { transform: translateY(0); }
}
@keyframes lg-unlock {
0%, 55% { background: rgba(255, 255, 255, .12); color: rgba(255, 255, 255, .72); }
56%, 88% { background: $color; color: $subject-cream; }
89%, 100% { background: rgba(255, 255, 255, .12); color: rgba(255, 255, 255, .72); }
}
@keyframes lg-lock-off {
0%, 55% { transform: scale(1); opacity: 1; }
56%, 88% { transform: scale(.4); opacity: 0; }
89%, 100% { transform: scale(1); opacity: 1; }
}
@keyframes lg-why-a {
0%, 55% { opacity: 1; }
56%, 88% { opacity: 0; }
89%, 100% { opacity: 1; }
}
@keyframes lg-why-b {
0%, 55% { opacity: 0; transform: translateY(3px); }
56% { opacity: 1; transform: translateY(3px); }
64%, 88% { opacity: 1; transform: translateY(0); }
89%, 100% { opacity: 0; transform: translateY(3px); }
}
.lg.is-demo {
.lg__row[data-row="1"] .lg__tick { animation-name: lg-on-1; }
.lg__row[data-row="2"] .lg__tick { animation-name: lg-on-2; }
.lg__row[data-row="3"] .lg__tick { animation-name: lg-on-3; }
.lg__tick {
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.lg__row[data-row="1"] .lg__box { animation-name: lg-pop-1; }
.lg__row[data-row="2"] .lg__box { animation-name: lg-pop-2; }
.lg__row[data-row="3"] .lg__box { animation-name: lg-pop-3; }
.lg__box {
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.lg__next {
animation-name: lg-unlock;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.lg__lock {
animation-name: lg-lock-off;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.lg__why-a {
animation-name: lg-why-a;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.lg__why-b {
animation-name: lg-why-b;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
}
@media (max-width: 340px) {
.lg { padding: $sp-2; font-size: 11px; }
.lg__h { margin-bottom: $sp-1; }
.lg__box { padding: 0 $sp-2; font-size: 11px; }
.lg__foot { margin-top: $sp-1; }
}
@media (prefers-reduced-motion: reduce) {
.lg.is-demo .lg__tick,
.lg.is-demo .lg__box,
.lg.is-demo .lg__next,
.lg.is-demo .lg__lock,
.lg.is-demo .lg__why-a,
.lg.is-demo .lg__why-b { animation: none !important; transform: none !important; }
// 움직임만 끄고 판정 결과는 남긴다
.lg.is-demo .lg__tick { background: $color !important; color: $subject-cream !important; }
.lg.is-demo .lg__next { background: $color !important; color: $subject-cream !important; }
.lg.is-demo .lg__lock { display: none !important; }
.lg.is-demo .lg__why-a { opacity: 0 !important; }
.lg.is-demo .lg__why-b { opacity: 1 !important; }
}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);
}
.lg {
box-sizing: border-box;
width: min(304px, 92vw);
padding: 12px;
border: 1px solid rgb(23.5764705882%, 22.0862745098%, 22.9960784314%);
border-radius: 16px;
background: #17141a;
color: #fff7e6;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.3;
}
.lg__h {
margin: 0 0 8px;
color: rgba(255, 255, 255, 0.56);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
}
.lg__list {
display: grid;
gap: 4px;
margin: 0;
padding: 0;
list-style: none;
}
.lg__box {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
margin: 0;
padding: 4px 8px;
border: 1px solid rgb(23.5764705882%, 22.0862745098%, 22.9960784314%);
border-radius: 8px;
background: rgb(16.2980392157%, 14.9647058824%, 16.5960784314%);
color: inherit;
font-family: inherit;
font-size: 12px;
line-height: 1.3;
text-align: left;
cursor: pointer;
}
.lg__box:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.lg__tick {
display: inline-flex;
flex: 0 0 auto;
width: 16px;
height: 16px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.1);
color: transparent;
}
.lg__label {
color: rgba(255, 255, 255, 0.74);
}
.lg__row.is-on .lg__tick {
background: #2f6df6;
color: #fff7e6;
}
.lg__row.is-on .lg__label {
color: #fff7e6;
}
.lg__foot {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.lg__why {
position: relative;
margin: 0;
min-height: 16px;
font-size: 11px;
}
.lg__why-a, .lg__why-b {
position: absolute;
inset: 0;
}
.lg__why-a {
color: rgba(255, 255, 255, 0.5);
}
.lg__why-b {
color: #4cd4a6;
opacity: 0;
}
.lg.is-ready .lg__why-a {
opacity: 0;
}
.lg.is-ready .lg__why-b {
opacity: 1;
}
.lg__next {
display: inline-flex;
align-items: center;
gap: 4px;
margin: 0;
padding: 4px 12px;
border: 0;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.72);
font-family: inherit;
font-size: 12px;
font-weight: 700;
line-height: 1.6;
cursor: not-allowed;
}
.lg__next:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.lg__lock {
display: inline-flex;
width: 13px;
height: 13px;
}
.lg.is-ready .lg__next {
background: #2f6df6;
color: #fff7e6;
cursor: pointer;
}
.lg.is-ready .lg__lock {
display: none;
}
.lg.is-sent .lg__next-t {
color: #4cd4a6;
}
.lg.is-nudge .lg__why-a {
color: #ffe0d8;
}
@keyframes lg-on-1 {
0%, 11% {
background: rgba(255, 255, 255, 0.1);
color: transparent;
}
12%, 100% {
background: #2f6df6;
color: #fff7e6;
}
}
@keyframes lg-on-2 {
0%, 31% {
background: rgba(255, 255, 255, 0.1);
color: transparent;
}
32%, 100% {
background: #2f6df6;
color: #fff7e6;
}
}
@keyframes lg-on-3 {
0%, 51% {
background: rgba(255, 255, 255, 0.1);
color: transparent;
}
52%, 88% {
background: #2f6df6;
color: #fff7e6;
}
89%, 100% {
background: rgba(255, 255, 255, 0.1);
color: transparent;
}
}
@keyframes lg-pop-1 {
0%, 8% {
transform: translateY(0);
}
14% {
transform: translateY(-3px);
}
22%, 100% {
transform: translateY(0);
}
}
@keyframes lg-pop-2 {
0%, 28% {
transform: translateY(0);
}
34% {
transform: translateY(-3px);
}
42%, 100% {
transform: translateY(0);
}
}
@keyframes lg-pop-3 {
0%, 48% {
transform: translateY(0);
}
54% {
transform: translateY(-3px);
}
62%, 88% {
transform: translateY(0);
}
100% {
transform: translateY(0);
}
}
@keyframes lg-unlock {
0%, 55% {
background: rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.72);
}
56%, 88% {
background: #2f6df6;
color: #fff7e6;
}
89%, 100% {
background: rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.72);
}
}
@keyframes lg-lock-off {
0%, 55% {
transform: scale(1);
opacity: 1;
}
56%, 88% {
transform: scale(0.4);
opacity: 0;
}
89%, 100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes lg-why-a {
0%, 55% {
opacity: 1;
}
56%, 88% {
opacity: 0;
}
89%, 100% {
opacity: 1;
}
}
@keyframes lg-why-b {
0%, 55% {
opacity: 0;
transform: translateY(3px);
}
56% {
opacity: 1;
transform: translateY(3px);
}
64%, 88% {
opacity: 1;
transform: translateY(0);
}
89%, 100% {
opacity: 0;
transform: translateY(3px);
}
}
.lg.is-demo .lg__row[data-row="1"] .lg__tick {
animation-name: lg-on-1;
}
.lg.is-demo .lg__row[data-row="2"] .lg__tick {
animation-name: lg-on-2;
}
.lg.is-demo .lg__row[data-row="3"] .lg__tick {
animation-name: lg-on-3;
}
.lg.is-demo .lg__tick {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.lg.is-demo .lg__row[data-row="1"] .lg__box {
animation-name: lg-pop-1;
}
.lg.is-demo .lg__row[data-row="2"] .lg__box {
animation-name: lg-pop-2;
}
.lg.is-demo .lg__row[data-row="3"] .lg__box {
animation-name: lg-pop-3;
}
.lg.is-demo .lg__box {
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.lg.is-demo .lg__next {
animation-name: lg-unlock;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.lg.is-demo .lg__lock {
animation-name: lg-lock-off;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.lg.is-demo .lg__why-a {
animation-name: lg-why-a;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.lg.is-demo .lg__why-b {
animation-name: lg-why-b;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
@media (max-width: 340px) {
.lg {
padding: 8px;
font-size: 11px;
}
.lg__h {
margin-bottom: 4px;
}
.lg__box {
padding: 0 8px;
font-size: 11px;
}
.lg__foot {
margin-top: 4px;
}
}
@media (prefers-reduced-motion: reduce) {
.lg.is-demo .lg__tick,
.lg.is-demo .lg__box,
.lg.is-demo .lg__next,
.lg.is-demo .lg__lock,
.lg.is-demo .lg__why-a,
.lg.is-demo .lg__why-b {
animation: none !important;
transform: none !important;
}
.lg.is-demo .lg__tick {
background: #2f6df6 !important;
color: #fff7e6 !important;
}
.lg.is-demo .lg__next {
background: #2f6df6 !important;
color: #fff7e6 !important;
}
.lg.is-demo .lg__lock {
display: none !important;
}
.lg.is-demo .lg__why-a {
opacity: 0 !important;
}
.lg.is-demo .lg__why-b {
opacity: 1 !important;
}
}// 실제 동작: 세 조건이 전부 켜져야 다음 버튼의 aria-disabled 가 false 로 바뀐다.
// 잠긴 동안 눌러도 단계는 안 넘어가고 이유 문구만 그대로 남는다.
var lg = document.getElementById('lg');
var lgBoxes = [].slice.call(lg.querySelectorAll('.lg__box'));
function lgSync() {
var done = lgBoxes.filter(function (b) { return b.getAttribute('aria-checked') === 'true'; }).length;
var ready = done === lgBoxes.length;
lg.classList.toggle('is-ready', ready);
var next = document.getElementById('lg-next');
next.setAttribute('aria-disabled', ready ? 'false' : 'true');
}
lgBoxes.forEach(function (b) {
b.addEventListener('click', function () {
lg.classList.remove('is-demo');
b.setAttribute('aria-checked', b.getAttribute('aria-checked') === 'true' ? 'false' : 'true');
b.parentNode.classList.toggle('is-on', b.getAttribute('aria-checked') === 'true');
lgSync();
});
});
document.getElementById('lg-next').addEventListener('click', function () {
lg.classList.remove('is-demo');
if (this.getAttribute('aria-disabled') === 'true') { lg.classList.add('is-nudge'); return; }
lg.classList.remove('is-nudge');
lg.classList.add('is-sent');
});
lgSync();import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./StepLockGate.module.scss";
export type StepLockGateProps = {
heading?: string;
rules?: string[];
lockedHint?: string;
readyHint?: string;
nextLabel?: string;
groupLabel?: string;
/** 격자 미리보기용 2초 자동 루프. 사람이 손대면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const DEFAULT_RULES = ["Accept the terms of service", "Accept the privacy notice", "I am 14 or older"];
function Tick() {
return (
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor">
<g>
<path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2" />
<path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32" />
</g>
</svg>
);
}
function Lock() {
return (
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor">
<g>
<path d="M216 96v112a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8V96a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8" opacity=".2" />
<path d="M208 80h-32V56a48 48 0 0 0-96 0v24H48a16 16 0 0 0-16 16v112a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16M96 56a32 32 0 0 1 64 0v24H96Zm112 152H48V96h160z" />
</g>
</svg>
);
}
export default function StepLockGate({
heading = "Step 2 · Required checks",
rules = DEFAULT_RULES,
lockedHint = "Required items are still open",
readyHint = "You can move to the next step",
nextLabel = "Next",
groupLabel = "Required checks for signup",
demo = true,
duration = 220,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: StepLockGateProps) {
const [on, setOn] = useState<boolean[]>(rules.map(() => false));
const [live, setLive] = useState(demo);
const [nudge, setNudge] = useState(false);
const ready = on.length > 0 && on.every(Boolean);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function toggle(i: number) {
setLive(false);
setOn((prev) => prev.map((v, n) => (n === i ? !v : v)));
}
function next() {
setLive(false);
// 잠긴 동안 눌러도 단계는 안 넘어간다 — 이유만 강조한다
setNudge(!ready);
}
const cls = [styles.lg, live ? styles.isDemo : "", ready ? styles.isReady : "", nudge ? styles.isNudge : ""]
.filter(Boolean)
.join(" ");
return (
<section className={cls} style={vars} aria-label={groupLabel}>
<p className={styles.h}>{heading}</p>
<ul className={styles.list}>
{rules.map((r, i) => (
<li key={r} className={[styles.row, on[i] ? styles.isOn : ""].filter(Boolean).join(" ")} data-row={i + 1}>
<button className={styles.box} type="button" role="checkbox" aria-checked={on[i]} onClick={() => toggle(i)}>
<span className={styles.tick} aria-hidden="true"><Tick /></span>
<span className={styles.label}>{r}</span>
</button>
</li>
))}
</ul>
<div className={styles.foot}>
<p className={styles.why}>
<span className={styles.whyA}>{lockedHint}</span>
<span className={styles.whyB}>{readyHint}</span>
</p>
<button className={styles.next} type="button" aria-disabled={!ready} onClick={next}>
{!ready && <span className={styles.lock} aria-hidden="true"><Lock /></span>}
<span className={styles.nextT}>{nextLabel}</span>
</button>
</div>
</section>
);
}01 조건이 차야 풀리는 다음 단계 잠금 — 측정 누적면적 8.435% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 회원가입 단계 ui 9가지 — 갈라지고 이어서 쓰는 폼
이 데모가 속한 글
회원가입 단계 ui 9가지 — 갈라지고 이어서 쓰는 폼회원가입 단계 ui는 한 번에 다 보여 주지 않고 몇 장의 화면으로 쪼개어 순서대로 받아 내는 입력 방식입니다. 아홉 개 전부 값을 실제로 읽고 검사하고 저장하며, 칸을 채우지 않으면 다음 장으로 안 넘어가고 탭을 이 글의 데모 전부 보기


