08비활성 흐림 고정 전환 — 비활성 값이 켜지면 박스의 밝기 값이 낮아지고 체크 값이 더는 바뀌지 않는 방식
비활성 값이 켜지는 순간 박스의 밝기 값이 낮아지고, 그 뒤로는 클릭이 와도 체크 값이 바뀌지 않습니다.
쓰임조건이 충족되지 않은 옵션, 다른 항목에 종속된 선택지
disabledfilterpointer-events

<main class="stage">
<div class="cbl is-demo" id="cbl">
<label class="cbl__row" id="cblRow">
<input type="checkbox" class="cbl__input" id="cblInput" disabled checked>
<span class="cbl__box" aria-hidden="true">
<svg viewBox="0 0 20 20" width="14" height="14"><path d="M4 10.5l3.5 3.5L16 5.5" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<span class="cbl__text">관리자 전용 설정</span>
</label>
<button type="button" class="cbl__toggle" id="cblToggle" aria-pressed="true">
<svg class="cbl__lock" viewBox="0 0 24 24" width="16" height="16">
<rect x="5" y="11" width="14" height="10" rx="2" fill="currentColor"/>
<path id="cblShackle" class="cbl__shackle" d="M8 11V7a4 4 0 0 1 8 0v4" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
</svg>
<span id="cblToggleText">잠금 해제</span>
</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: index.html 이 자물쇠 걸쇠(shackle)의 회전각을 고정 축 기준으로 easeOutCubic 적분해
// rotate() 값을 만든다 — 잠금 해제 버튼을 누르면 몸통은 그대로, 걸쇠만 계산된 각도로 열린다.
@include stage($stage-ink);
.cbl {
display: grid;
gap: $sp-4;
width: 100%;
max-width: 260px;
}
.cbl__row {
display: flex;
align-items: center;
gap: $sp-3;
padding: $sp-3;
border-radius: $r-sm;
transition: filter $dur-base $ease-out;
}
.cbl__row--locked {
filter: grayscale(.6) brightness(.7) blur(.3px);
pointer-events: none;
}
.cbl__input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
}
.cbl__box {
flex: 0 0 auto;
width: 22px;
height: 22px;
border-radius: $r-sm;
border: 2px solid rgba($subject-cream, .4);
color: $subject-cream;
display: grid;
place-items: center;
box-sizing: border-box;
svg { opacity: 0; }
}
.cbl__input:checked + .cbl__box {
background: $subject-blue;
border-color: $subject-blue;
svg { opacity: 1; }
}
.cbl__text {
color: $subject-cream;
font-size: 14px;
font-weight: 600;
}
.cbl__toggle {
justify-self: start;
display: flex;
align-items: center;
gap: $sp-2;
padding: $sp-2 $sp-4;
border: 1px solid rgba($subject-cream, .3);
border-radius: $r-sm;
background: transparent;
color: $subject-cream;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.cbl__lock {
color: $subject-cream;
}
.cbl__shackle {
will-change: transform;
}
@media (max-width: 340px) {
.cbl__text { font-size: 13px; }
}
@media (prefers-reduced-motion: reduce) {
.cbl__row, .cbl__shackle { 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);
}
.cbl {
display: grid;
gap: 16px;
width: 100%;
max-width: 260px;
}
.cbl__row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 8px;
transition: filter 300ms ease-out;
}
.cbl__row--locked {
filter: grayscale(0.6) brightness(0.7) blur(0.3px);
pointer-events: none;
}
.cbl__input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
}
.cbl__box {
flex: 0 0 auto;
width: 22px;
height: 22px;
border-radius: 8px;
border: 2px solid rgba(255, 247, 230, 0.4);
color: #fff7e6;
display: grid;
place-items: center;
box-sizing: border-box;
}
.cbl__box svg {
opacity: 0;
}
.cbl__input:checked + .cbl__box {
background: #2f6df6;
border-color: #2f6df6;
}
.cbl__input:checked + .cbl__box svg {
opacity: 1;
}
.cbl__text {
color: #fff7e6;
font-size: 14px;
font-weight: 600;
}
.cbl__toggle {
justify-self: start;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border: 1px solid rgba(255, 247, 230, 0.3);
border-radius: 8px;
background: transparent;
color: #fff7e6;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.cbl__lock {
color: #fff7e6;
}
.cbl__shackle {
will-change: transform;
}
@media (max-width: 340px) {
.cbl__text {
font-size: 13px;
}
}
@media (prefers-reduced-motion: reduce) {
.cbl__row, .cbl__shackle {
transition: none;
}
}(function () {
var LOCK_MS = 360; // 계산 겹 상수: 걸쇠가 열리는 회전이 끝나는 데 걸리는 실제 시간(ms)
var UNLOCK_ANGLE = -42; // deg, 걸쇠가 열릴 때 돌아가는 최대 각도
var SHACKLE_PIVOT_X = 8, SHACKLE_PIVOT_Y = 11; // svg 좌표계 안의 회전 축(걸쇠 아래쪽 끝)
var CYCLE_MS = 1300;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var root = document.getElementById('cbl');
var row = document.getElementById('cblRow');
var input = document.getElementById('cblInput');
var toggle = document.getElementById('cblToggle');
var toggleText = document.getElementById('cblToggleText');
var shackle = document.getElementById('cblShackle');
var isDemo = true;
var locked = true;
var animating = false, animStart = 0, angleFrom = 0, angleTo = 0;
function human() {
if (isDemo) { root.classList.remove('is-demo'); isDemo = false; }
}
function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
function currentAngle(ts) {
if (!animating) return angleTo;
var t = Math.min(1, (ts - animStart) / LOCK_MS);
return angleFrom + (angleTo - angleFrom) * easeOutCubic(t);
}
function apply(nextLocked, ts) {
locked = nextLocked;
input.disabled = locked;
row.classList.toggle('cbl__row--locked', locked);
toggle.setAttribute('aria-pressed', String(locked));
toggleText.textContent = locked ? '잠금 해제' : '잠금';
angleFrom = currentAngle(ts);
angleTo = locked ? 0 : UNLOCK_ANGLE;
animStart = ts;
animating = true;
}
toggle.addEventListener('click', function () {
human();
apply(!locked, performance.now());
});
var cycleStart = 0;
var lastState = true;
function loop(ts) {
if (!cycleStart) { cycleStart = ts; apply(true, ts); }
if (isDemo) {
var step = Math.floor((ts - cycleStart) / CYCLE_MS) % 2 === 1;
if (step !== lastState) { lastState = step; apply(!step, ts); }
}
var angle = currentAngle(ts);
// 계산 겹: 걸쇠 회전을 고정 축(SHACKLE_PIVOT_X, Y) 기준으로 계산해 자물쇠 몸통은 그대로,
// 걸쇠만 열리는 자연스러운 회전을 만든다
shackle.style.transform = 'rotate(' + angle.toFixed(2) + 'deg)';
shackle.style.transformOrigin = SHACKLE_PIVOT_X + 'px ' + SHACKLE_PIVOT_Y + 'px';
if (animating && Math.abs(angle - angleTo) < 0.01) animating = false;
requestAnimationFrame(loop);
}
if (reduce) { LOCK_MS = 1; }
requestAnimationFrame(loop);
})();// DisabledDimLock — vanilla 판(run/368/demo/08-custom-checkbox-disabled-dim-lock/index.html) 의
// React 포트. 자물쇠 걸쇠(shackle)의 회전각을 고정 축 기준으로 easeOutCubic 적분해 rotate() 값을
// 만든다 — 잠금 해제 버튼을 누르면 몸통은 그대로, 걸쇠만 계산된 각도로 열린다.
import { useEffect, useRef } from "react";
import styles from "./DisabledDimLock.module.scss";
type Box<T> = { current: T };
export default function DisabledDimLock() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement>;
const rowRef = useRef(null) as unknown as Box<HTMLLabelElement>;
const inputRef = useRef(null) as unknown as Box<HTMLInputElement>;
const toggleRef = useRef(null) as unknown as Box<HTMLButtonElement>;
const toggleTextRef = useRef(null) as unknown as Box<HTMLSpanElement>;
const shackleRef = useRef(null) as unknown as Box<SVGPathElement>;
const rafRef = useRef(0) as unknown as Box<number>;
useEffect(() => {
let LOCK_MS = 360; // 계산 겹 상수: 걸쇠가 열리는 회전이 끝나는 데 걸리는 실제 시간(ms)
const UNLOCK_ANGLE = -42; // deg, 걸쇠가 열릴 때 돌아가는 최대 각도
const SHACKLE_PIVOT_X = 8, SHACKLE_PIVOT_Y = 11; // svg 좌표계 안의 회전 축(걸쇠 아래쪽 끝)
const CYCLE_MS = 1300;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const root = rootRef.current;
const row = rowRef.current;
const input = inputRef.current;
const toggle = toggleRef.current;
const toggleText = toggleTextRef.current;
const shackle = shackleRef.current;
if (!root || !row || !input || !toggle || !toggleText || !shackle) return;
let isDemo = true;
let locked = true;
let animating = false, animStart = 0, angleFrom = 0, angleTo = 0;
function human() {
if (isDemo) { root!.classList.remove(styles.isDemo); isDemo = false; }
}
function easeOutCubic(t: number) { return 1 - Math.pow(1 - t, 3); }
function currentAngle(ts: number) {
if (!animating) return angleTo;
const t = Math.min(1, (ts - animStart) / LOCK_MS);
return angleFrom + (angleTo - angleFrom) * easeOutCubic(t);
}
function apply(nextLocked: boolean, ts: number) {
locked = nextLocked;
input!.disabled = locked;
row!.classList.toggle(styles.rowLocked, locked);
toggle!.setAttribute("aria-pressed", String(locked));
toggleText!.textContent = locked ? "잠금 해제" : "잠금";
angleFrom = currentAngle(ts);
angleTo = locked ? 0 : UNLOCK_ANGLE;
animStart = ts;
animating = true;
}
function onToggleClick() { human(); apply(!locked, performance.now()); }
toggle.addEventListener("click", onToggleClick);
let cycleStart = 0;
let lastState = true;
function loop(ts: number) {
if (!cycleStart) { cycleStart = ts; apply(true, ts); }
if (isDemo) {
const step = Math.floor((ts - cycleStart) / CYCLE_MS) % 2 === 1;
if (step !== lastState) { lastState = step; apply(!step, ts); }
}
const angle = currentAngle(ts);
// 계산 겹: 걸쇠 회전을 고정 축(SHACKLE_PIVOT_X, Y) 기준으로 계산해 자물쇠 몸통은 그대로,
// 걸쇠만 열리는 자연스러운 회전을 만든다
shackle!.style.transform = "rotate(" + angle.toFixed(2) + "deg)";
shackle!.style.transformOrigin = SHACKLE_PIVOT_X + "px " + SHACKLE_PIVOT_Y + "px";
if (animating && Math.abs(angle - angleTo) < 0.01) animating = false;
rafRef.current = requestAnimationFrame(loop);
}
if (reduce) { LOCK_MS = 1; }
rafRef.current = requestAnimationFrame(loop);
return () => {
toggle.removeEventListener("click", onToggleClick);
cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.cbl + " " + styles.isDemo} ref={rootRef}>
<label className={styles.cblRow} ref={rowRef}>
<input type="checkbox" className={styles.cblInput} ref={inputRef} disabled defaultChecked />
<span className={styles.cblBox} aria-hidden="true">
<svg viewBox="0 0 20 20" width={14} height={14}>
<path d="M4 10.5l3.5 3.5L16 5.5" fill="none" stroke="currentColor" strokeWidth={2.6} strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
<span className={styles.cblText}>관리자 전용 설정</span>
</label>
<button type="button" className={styles.cblToggle} ref={toggleRef} aria-pressed="true">
<svg className={styles.cblLock} viewBox="0 0 24 24" width={16} height={16}>
<rect x={5} y={11} width={14} height={10} rx={2} fill="currentColor" />
<path ref={shackleRef} className={styles.cblShackle} d="M8 11V7a4 4 0 0 1 8 0v4" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" />
</svg>
<span ref={toggleTextRef}>잠금 해제</span>
</button>
</div>
</main>
);
}08 비활성 흐림 고정 전환 — 비활성 값이 켜지면 박스의 밝기 값이 낮아지고 체크 값이 더는 바뀌지 않는 방식 — 측정 누적면적 1.173% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 커스텀 체크박스 9종 — 체크 값으로 바뀌는 아홉 가지 방식
이 데모가 속한 글
커스텀 체크박스 9종 — 체크 값으로 바뀌는 아홉 가지 방식커스텀 체크박스 아홉 종을 값 기준으로 나누면, 아홉 가지 중 하나는 체크 값이 켜지면 체크 표시의 선 길이 값이 0에서 끝까지 그려지고, 하나는 같은 값이 바뀌는 순간 박스의 크기 값이 잠깐 커졌다가 튕기듯 가라앉 이 글의 데모 전부 보기
이 효과가 마음에 들면 — 비슷한 데모
입력 UI07 완료 값 빛남 전환 — 번호의 완료 값이 켜지면 테두리의 빛남 값이 서서히 올라가며 칸이 잠금 값으로 바뀌는 방식카드 번호 입력 UI 9종 — 자릿수 값마다 반응이 달라지는 아홉 가지 방식
입력 UI08 단위 플립 전환 — 단위가 바뀐 상태가 되면 카드가 뒤집히며 90도 지점에서 표시 값이 바뀌는 스텝퍼숫자 스텝퍼 UI 패턴 9종 — 길게 누를수록 빨라지는 것부터 눈금에 붙는 것까지
입력 UI04 흔한 값 감점 — 입력 값이 미리 정해둔 흔한 값 목록과 같은 상태가 되면 강도 값이 깎이는 방식비밀번호 강도 UI 9종 — 강도 값이 계산되는 아홉 가지 방식