07条件充足比率リアルタイム反映 — 複数の条件の値が真になる数の値の比率が強度の値に変わる方式
複数の条件の値がそれぞれ真になるかリアルタイムで検査され、真になった条件の値の数が全体の条件数の値に対する比率として強度の値に反映されます。
使いどころ要求条件を項目ごとに案内する登録フォーム、社内ポリシーの条件を並べるアカウント設定
CHECKS 기준 4종.cx__item--onSVG path 체크 아이콘

<main class="stage">
<div class="cx is-demo" id="cx">
<label class="cx__field">
<input class="cx__input" id="cxInput" type="text" autocomplete="off" spellcheck="false" value="">
</label>
<ul class="cx__list" id="cxList">
<li class="cx__item" data-key="len">
<span class="cx__badge"><svg viewBox="0 0 20 20" width="12" height="12" aria-hidden="true"><path d="M4 10.5l3.5 3.5L16 5.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="cx__label">8文字以上</span>
</li>
<li class="cx__item" data-key="upper">
<span class="cx__badge"><svg viewBox="0 0 20 20" width="12" height="12" aria-hidden="true"><path d="M4 10.5l3.5 3.5L16 5.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="cx__label">大文字を含む</span>
</li>
<li class="cx__item" data-key="digit">
<span class="cx__badge"><svg viewBox="0 0 20 20" width="12" height="12" aria-hidden="true"><path d="M4 10.5l3.5 3.5L16 5.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="cx__label">数字を含む</span>
</li>
<li class="cx__item" data-key="special">
<span class="cx__badge"><svg viewBox="0 0 20 20" width="12" height="12" aria-hidden="true"><path d="M4 10.5l3.5 3.5L16 5.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="cx__label">記号を含む</span>
</li>
</ul>
<div class="cx__track"><div class="cx__fill" id="cxFill"></div></div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 4개 기준(길이·대문자·숫자·특수문자)을 각각 판정해 참인 개수를 세고,
// 그 비율(개수/4)로 막대를 채운다 — 배지는 SVG 체크(글리프 아님)로 표시한다.
@include stage($stage-orange);
.cx {
width: 100%;
max-width: 320px;
display: grid;
gap: $sp-3;
}
.cx__field { display: block; }
.cx__input {
width: 100%;
box-sizing: border-box;
padding: $sp-3 $sp-4;
border-radius: $r-control;
border: 0;
background: $subject-cream;
color: $subject-ink;
font: 500 16px/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
letter-spacing: .04em;
-webkit-text-security: disc;
&:focus { outline: 2px solid $subject-ink; outline-offset: 2px; }
}
.cx__list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: $sp-2;
}
.cx__item {
display: flex;
align-items: center;
gap: $sp-2;
font: 600 13px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: rgba($subject-ink, .68);
}
.cx__badge {
width: 20px;
height: 20px;
border-radius: $r-xs;
display: grid;
place-items: center;
background: rgba($subject-ink, .12);
color: transparent;
transition: background-color $dur-quick $ease-out, color $dur-quick $ease-out;
svg {
transform: scale(.5);
opacity: 0;
transition: transform $dur-quick $ease-spring, opacity $dur-quick $ease-out;
}
}
.cx__item--on {
color: $subject-ink;
.cx__badge {
background: $subject-ink;
color: $subject-cream;
svg { transform: scale(1); opacity: 1; }
}
}
.cx__track {
position: relative;
height: $sp-3;
border-radius: $r-sm;
background: rgba($subject-ink, .12);
overflow: hidden;
}
.cx__fill {
position: absolute;
inset: 0;
border-radius: $r-sm;
background: $subject-ink;
transform: scaleX(0);
transform-origin: left center;
transition: transform $dur-base $ease-out;
}
@media (max-width: 340px) {
.cx { max-width: 260px; }
.cx__input { padding: $sp-2 $sp-3; font-size: 14px; }
.cx__item { font-size: 12px; }
}
@media (prefers-reduced-motion: reduce) {
.cx__badge, .cx__fill { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cx {
width: 100%;
max-width: 320px;
display: grid;
gap: 12px;
}
.cx__field {
display: block;
}
.cx__input {
width: 100%;
box-sizing: border-box;
padding: 12px 16px;
border-radius: 12px;
border: 0;
background: #fff7e6;
color: #17141a;
font: 500 16px/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
letter-spacing: 0.04em;
-webkit-text-security: disc;
}
.cx__input:focus {
outline: 2px solid #17141a;
outline-offset: 2px;
}
.cx__list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 8px;
}
.cx__item {
display: flex;
align-items: center;
gap: 8px;
font: 600 13px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: rgba(23, 20, 26, 0.68);
}
.cx__badge {
width: 20px;
height: 20px;
border-radius: 4px;
display: grid;
place-items: center;
background: rgba(23, 20, 26, 0.12);
color: transparent;
transition: background-color 220ms ease-out, color 220ms ease-out;
}
.cx__badge svg {
transform: scale(0.5);
opacity: 0;
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 220ms ease-out;
}
.cx__item--on {
color: #17141a;
}
.cx__item--on .cx__badge {
background: #17141a;
color: #fff7e6;
}
.cx__item--on .cx__badge svg {
transform: scale(1);
opacity: 1;
}
.cx__track {
position: relative;
height: 12px;
border-radius: 8px;
background: rgba(23, 20, 26, 0.12);
overflow: hidden;
}
.cx__fill {
position: absolute;
inset: 0;
border-radius: 8px;
background: #17141a;
transform: scaleX(0);
transform-origin: left center;
transition: transform 300ms ease-out;
}
@media (max-width: 340px) {
.cx {
max-width: 260px;
}
.cx__input {
padding: 8px 12px;
font-size: 14px;
}
.cx__item {
font-size: 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.cx__badge, .cx__fill {
transition: none;
}
}(function () {
var root = document.getElementById('cx');
var input = document.getElementById('cxInput');
var items = root.querySelectorAll('.cx__item');
var fill = document.getElementById('cxFill');
// 계산 겹: 4개 기준을 각각 판정해 참인 개수를 세고, 그 비율(개수/4)로
// 막대 채움을 맞춘다 — 배지는 개수가 아니라 각 기준의 참/거짓으로 켜진다.
var CHECKS = {
len: function (v) { return v.length >= 8; },
upper: function (v) { return /[A-Z]/.test(v); },
digit: function (v) { return /[0-9]/.test(v); },
special: function (v) { return /[^A-Za-z0-9]/.test(v); }
};
function paint(value) {
var met = 0;
items.forEach(function (li) {
var key = li.getAttribute('data-key');
var ok = CHECKS[key](value);
li.classList.toggle('cx__item--on', ok);
if (ok) met++;
});
fill.style.transform = 'scaleX(' + (met / items.length) + ')';
}
input.addEventListener('input', function () {
root.classList.remove('is-demo');
stopAutoplay();
paint(input.value);
});
paint(input.value);
// 자동 루프: 시드 고정 문자열 배열로 기준을 하나씩 채웠다 되돌린다.
var SEQ = ['', 'short', 'longenough', 'longenoughA', 'longenoughA1', 'longenoughA1!',
'longenoughA1', 'longenoughA', 'longenough', 'short', ''];
var si = 0;
var autoTimer = null;
function autoTick() { input.value = SEQ[si]; paint(SEQ[si]); si = (si + 1) % SEQ.length; }
function stopAutoplay() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } }
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) { autoTimer = setInterval(autoTick, 380); autoTick(); }
})();import { useEffect, useRef } from "react";
import styles from "./CriteriaChecklistLive.module.scss";
type Box<T> = { current: T };
const CHECKS: Record<string, (v: string) => boolean> = {
len: (v) => v.length >= 8,
upper: (v) => /[A-Z]/.test(v),
digit: (v) => /[0-9]/.test(v),
special: (v) => /[^A-Za-z0-9]/.test(v),
};
const CHECK_ICON = "M4 10.5l3.5 3.5L16 5.5";
export default function CriteriaChecklistLive() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement | null>;
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const input = root.querySelector(`.${styles.input}`) as HTMLInputElement | null;
const items = root.querySelectorAll(`.${styles.item}`);
const fill = root.querySelector(`.${styles.fill}`) as HTMLElement | null;
if (!input || !fill || items.length === 0) return;
// 계산 겹: 4개 기준을 각각 판정해 참인 개수를 세고, 그 비율(개수/4)로
// 막대 채움을 맞춘다 — 배지는 개수가 아니라 각 기준의 참/거짓으로 켜진다.
function paint(value: string) {
let met = 0;
items.forEach((li) => {
const key = (li as HTMLElement).getAttribute("data-key") || "";
const ok = CHECKS[key] ? CHECKS[key](value) : false;
li.classList.toggle(styles["item--on"], ok);
if (ok) met++;
});
fill!.style.transform = "scaleX(" + met / items.length + ")";
}
function onInput() {
root!.classList.remove(styles["is-demo"]);
stopAutoplay();
paint(input!.value);
}
input.addEventListener("input", onInput);
paint(input.value);
// 자동 루프: 시드 고정 문자열 배열로 기준을 하나씩 채웠다 되돌린다.
const SEQ = ["", "short", "longenough", "longenoughA", "longenoughA1", "longenoughA1!",
"longenoughA1", "longenoughA", "longenough", "short", ""];
let si = 0;
let autoTimer: number | null = null;
function autoTick() { input!.value = SEQ[si]; paint(SEQ[si]); si = (si + 1) % SEQ.length; }
function stopAutoplay() { if (autoTimer !== null) { window.clearInterval(autoTimer); autoTimer = null; } }
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduce) { autoTimer = window.setInterval(autoTick, 380); autoTick(); }
return () => {
stopAutoplay();
input.removeEventListener("input", onInput);
};
}, []);
const rows: Array<[string, string]> = [
["len", "8자 이상"],
["upper", "대문자 포함"],
["digit", "숫자 포함"],
["special", "특수문자 포함"],
];
return (
<div ref={rootRef} className={`${styles.stage} ${styles.cx} ${styles["is-demo"]}`}>
<label className={styles.field}>
<input className={styles.input} type="text" autoComplete="off" spellCheck={false} defaultValue="" />
</label>
<ul className={styles.list}>
{rows.map(([key, label]) => (
<li key={key} className={styles.item} data-key={key}>
<span className={styles.badge}>
<svg viewBox="0 0 20 20" width="12" height="12" aria-hidden="true">
<path d={CHECK_ICON} fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
<span>{label}</span>
</li>
))}
</ul>
<div className={styles.track}><div className={styles.fill} /></div>
</div>
);
}07 条件充足比率リアルタイム反映 — 複数の条件の値が真になる数の値の比率が強度の値に変わる方式 — 計測 累積面積 4.904% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワード強度チェック UI 9選 — 強度の値が計算される仕組み
このデモが属する記事
パスワード強度チェック UI 9選 — 強度の値が計算される仕組み9つのうち1つは文字種の値がいくつ混ざっているかで強度の値が決まり、1つはよくある値のリストと同じ状態になると強度の値が減り、もう1つは確認入力の値と元の値が食い違い始める位置の値をリアルタイムで見つけます。パスワード強度チェック、9つの作 この記事のデモをすべて見る


