09연속 색 값 전환 — 강도 값이 단계로 끊기지 않고 연속된 값으로 바뀌며 색상 값도 함께 매끄럽게 이어지는 방식
강도 값이 정해진 몇 단계로 뚝뚝 끊기지 않고 연속된 값으로 바뀌면서 그 값에 대응하는 색상 값도 함께 매끄럽게 이어집니다.
쓰임세밀한 시각 피드백을 주는 계정 설정 화면, 디자인이 강조된 가입 폼
STOPS 색 정지점 3개colorAt(t)scoreT(value)

<main class="stage">
<div class="cm is-demo" id="cm">
<label class="cm__field">
<input class="cm__input" id="cmInput" type="text" autocomplete="off" spellcheck="false" value="">
</label>
<div class="cm__track"><div class="cm__fill" id="cmFill"></div></div>
<div class="cm__readout">정도 <b id="cmPct">0</b>%</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 길이 비중·문자 종류 비중을 반씩 섞은 연속값(0~1)을 3개 색 정지점 사이에서
// RGB 선형보간해 매 입력마다 새 색을 계산한다 — 정해진 색 중 고르는 게 아니라 만든다.
@include stage($stage-ink);
.cm {
width: 100%;
max-width: 320px;
display: grid;
gap: $sp-4;
}
.cm__field { display: block; }
.cm__input {
width: 100%;
box-sizing: border-box;
padding: $sp-3 $sp-4;
border-radius: $r-control;
border: 0;
background: rgba($stage-paper, .12);
color: $subject-cream;
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-blue-light; outline-offset: 2px; }
}
.cm__track {
position: relative;
height: $sp-4;
border-radius: $r-sm;
background: rgba($stage-paper, .14);
overflow: hidden;
}
.cm__fill {
position: absolute;
inset: 0;
border-radius: $r-sm;
background: $error;
transform: scaleX(0);
transform-origin: left center;
transition: transform $dur-base $ease-out, background-color $dur-base $ease-out;
}
.cm__readout {
font: 600 13px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: $stage-paper;
text-align: center;
b { font-variant-numeric: tabular-nums; }
}
@media (max-width: 340px) {
.cm { max-width: 260px; }
.cm__input { padding: $sp-2 $sp-3; font-size: 14px; }
.cm__readout { font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.cm__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: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cm {
width: 100%;
max-width: 320px;
display: grid;
gap: 16px;
}
.cm__field {
display: block;
}
.cm__input {
width: 100%;
box-sizing: border-box;
padding: 12px 16px;
border-radius: 12px;
border: 0;
background: rgba(255, 255, 255, 0.12);
color: #fff7e6;
font: 500 16px/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
letter-spacing: 0.04em;
-webkit-text-security: disc;
}
.cm__input:focus {
outline: 2px solid #5b8cf8;
outline-offset: 2px;
}
.cm__track {
position: relative;
height: 16px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.14);
overflow: hidden;
}
.cm__fill {
position: absolute;
inset: 0;
border-radius: 8px;
background: #c2361a;
transform: scaleX(0);
transform-origin: left center;
transition: transform 300ms ease-out, background-color 300ms ease-out;
}
.cm__readout {
font: 600 13px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #ffffff;
text-align: center;
}
.cm__readout b {
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.cm {
max-width: 260px;
}
.cm__input {
padding: 8px 12px;
font-size: 14px;
}
.cm__readout {
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.cm__fill {
transition: none;
}
}(function () {
var root = document.getElementById('cm');
var input = document.getElementById('cmInput');
var fill = document.getElementById('cmFill');
var pctEl = document.getElementById('cmPct');
// 계산 겹: 길이 비중·문자 종류 비중을 반씩 섞어 연속값(0~1)을 만들고, 그 값을
// 3개 색 정지점(red→yellow→mint) 사이에서 RGB 선형보간해 매 입력마다 새로 계산한다.
var STOPS = [
{ t: 0, rgb: [194, 54, 26] },
{ t: 0.5, rgb: [255, 210, 63] },
{ t: 1, rgb: [76, 212, 166] }
];
function lerp(a, b, f) { return a + (b - a) * f; }
function colorAt(t) {
t = Math.max(0, Math.min(1, t));
var lo = STOPS[0], hi = STOPS[STOPS.length - 1];
for (var i = 0; i < STOPS.length - 1; i++) {
if (t >= STOPS[i].t && t <= STOPS[i + 1].t) { lo = STOPS[i]; hi = STOPS[i + 1]; break; }
}
var span = hi.t - lo.t || 1;
var f = (t - lo.t) / span;
var r = Math.round(lerp(lo.rgb[0], hi.rgb[0], f));
var g = Math.round(lerp(lo.rgb[1], hi.rgb[1], f));
var b = Math.round(lerp(lo.rgb[2], hi.rgb[2], f));
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
function classCount(value) {
var n = 0;
if (/[a-z]/.test(value)) n++;
if (/[A-Z]/.test(value)) n++;
if (/[0-9]/.test(value)) n++;
if (/[^A-Za-z0-9]/.test(value)) n++;
return n;
}
function scoreT(value) {
var lengthPart = Math.min(1, value.length / 20);
var classPart = classCount(value) / 4;
return 0.5 * lengthPart + 0.5 * classPart;
}
function paint(value) {
var t = scoreT(value);
fill.style.transform = 'scaleX(' + t + ')';
fill.style.backgroundColor = colorAt(t);
pctEl.textContent = String(Math.round(t * 100));
}
input.addEventListener('input', function () {
root.classList.remove('is-demo');
stopAutoplay();
paint(input.value);
});
paint(input.value);
// 자동 루프: 시드 고정 문자열 배열로 길이·문자 종류를 함께 늘렸다 줄인다.
var SEQ = ['', 'ab', 'abcdef', 'abcdefGH', 'abcdefGH12', 'abcdefGH12!@',
'abcdefGH12', 'abcdefGH', 'abcdef', 'ab', ''];
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 "./ColorGradientMorph.module.scss";
type Box<T> = { current: T };
type Stop = { t: number; rgb: [number, number, number] };
const STOPS: Stop[] = [
{ t: 0, rgb: [194, 54, 26] },
{ t: 0.5, rgb: [255, 210, 63] },
{ t: 1, rgb: [76, 212, 166] },
];
function lerp(a: number, b: number, f: number) { return a + (b - a) * f; }
function colorAt(t: number) {
t = Math.max(0, Math.min(1, t));
let lo = STOPS[0], hi = STOPS[STOPS.length - 1];
for (let i = 0; i < STOPS.length - 1; i++) {
if (t >= STOPS[i].t && t <= STOPS[i + 1].t) { lo = STOPS[i]; hi = STOPS[i + 1]; break; }
}
const span = hi.t - lo.t || 1;
const f = (t - lo.t) / span;
const r = Math.round(lerp(lo.rgb[0], hi.rgb[0], f));
const g = Math.round(lerp(lo.rgb[1], hi.rgb[1], f));
const b = Math.round(lerp(lo.rgb[2], hi.rgb[2], f));
return "rgb(" + r + "," + g + "," + b + ")";
}
export default function ColorGradientMorph() {
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 fill = root.querySelector(`.${styles.fill}`) as HTMLElement | null;
const pctEl = root.querySelector(`.${styles.pct}`) as HTMLElement | null;
if (!input || !fill || !pctEl) return;
function classCount(value: string) {
let n = 0;
if (/[a-z]/.test(value)) n++;
if (/[A-Z]/.test(value)) n++;
if (/[0-9]/.test(value)) n++;
if (/[^A-Za-z0-9]/.test(value)) n++;
return n;
}
// 계산 겹: 길이 비중·문자 종류 비중을 반씩 섞어 연속값(0~1)을 만들고, 그 값을
// 3개 색 정지점(red→yellow→mint) 사이에서 RGB 선형보간해 매 입력마다 새로 계산한다.
function scoreT(value: string) {
const lengthPart = Math.min(1, value.length / 20);
const classPart = classCount(value) / 4;
return 0.5 * lengthPart + 0.5 * classPart;
}
function paint(value: string) {
const t = scoreT(value);
fill!.style.transform = "scaleX(" + t + ")";
fill!.style.backgroundColor = colorAt(t);
pctEl!.textContent = String(Math.round(t * 100));
}
function onInput() {
root!.classList.remove(styles["is-demo"]);
stopAutoplay();
paint(input!.value);
}
input.addEventListener("input", onInput);
paint(input.value);
// 자동 루프: 시드 고정 문자열 배열로 길이·문자 종류를 함께 늘렸다 줄인다.
const SEQ = ["", "ab", "abcdef", "abcdefGH", "abcdefGH12", "abcdefGH12!@",
"abcdefGH12", "abcdefGH", "abcdef", "ab", ""];
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);
};
}, []);
return (
<div ref={rootRef} className={`${styles.stage} ${styles.cm} ${styles["is-demo"]}`}>
<label className={styles.field}>
<input className={styles.input} type="text" autoComplete="off" spellCheck={false} defaultValue="" />
</label>
<div className={styles.track}><div className={styles.fill} /></div>
<div className={styles.readout}>정도 <b className={styles.pct}>0</b>%</div>
</div>
);
}09 연속 색 값 전환 — 강도 값이 단계로 끊기지 않고 연속된 값으로 바뀌며 색상 값도 함께 매끄럽게 이어지는 방식 — 측정 누적면적 3.354% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 강도 UI 9종 — 강도 값이 계산되는 아홉 가지 방식
이 데모가 속한 글
비밀번호 강도 UI 9종 — 강도 값이 계산되는 아홉 가지 방식비밀번호 강도는 입력한 값이 얼마나 뚫기 쉬운지를 점수나 막대로 보여주는 계산인데, 아홉 개 중 하나는 문자 종류 값이 몇 가지 섞였는지에 따라 강도 값이 정해지고, 하나는 흔한 값 목록과 같은 상태가 되면 강도 값 이 글의 데모 전부 보기
이 효과가 마음에 들면 — 비슷한 데모
입력 UI08 패턴 인식 값 전환 — 앞자리 숫자 값이 정해진 패턴 값과 맞으면 옆의 도형 값이 다른 모양으로 바뀌는 방식카드 번호 입력 UI 9종 — 자릿수 값마다 반응이 달라지는 아홉 가지 방식
입력 UI09 모션 축소 대응 비교 전환 — 같은 체크 값 변화를 일반 판은 선 길이 값을 그려서, 축소 판은 중간 단계 없이 바로 보여주는 방식커스텀 체크박스 9종 — 체크 값으로 바뀌는 아홉 가지 방식
입력 UI06 HSL 슬라이더 — 색조 값에 따라 트랙 그라디언트를 다시 그리는 컬러피커컬러피커 UI 디자인 9종 — 휠부터 최근 색상까지