07色が数式でつながるバッジ型難易度選択
5つのバッジは固定された5色ではなく段階番号を色相角の計算式に入れて作った色のため、段階を増やすとバッジ間の色の間隔も再計算される。
使いどころ管理画面で難易度等級をタグのように付ける場所
badgeHuebadgeColorpaint

<main class="stage">
<div class="bhs is-demo" id="bhs" role="radiogroup" aria-label="難易度">
<button type="button" class="bhs__badge" role="radio" aria-checked="false" data-i="0" tabindex="0">簡単</button>
<button type="button" class="bhs__badge" role="radio" aria-checked="false" data-i="1" tabindex="-1">普通</button>
<button type="button" class="bhs__badge" role="radio" aria-checked="true" data-i="2" tabindex="-1">難しい</button>
<button type="button" class="bhs__badge" role="radio" aria-checked="false" data-i="3" tabindex="-1">とても難しい</button>
<button type="button" class="bhs__badge" role="radio" aria-checked="false" data-i="4" tabindex="-1">最高</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 변수 3개
$duration: $dur-quick;
$easing: $ease-out;
$color: $subject-ink;
// 계산 겹: badgeHue(i) 가 배지 색을 (기준 색상각 + 번호*간격) 수식으로 구한다(JS) — scss 는 배지 모양만
@include stage($stage-orange);
.bhs { display: flex; flex-wrap: wrap; gap: $sp-2; justify-content: center; max-width: 290px; }
.bhs__badge {
appearance: none; border: 2px solid transparent; border-radius: $r-pill;
padding: $sp-2 $sp-3; font-weight: 700; font-size: 12px; color: $color; cursor: pointer;
transition: transform $dur-quick $ease-pop, border-color $dur-quick $ease-out;
}
.bhs__badge:focus-visible { outline: 2px solid $subject-ink; outline-offset: 2px; }
.bhs__badge[aria-checked="true"] { border-color: $subject-ink; transform: scale(1.08); }
@media (max-width: 340px) {
.bhs { max-width: 250px; }
.bhs__badge { font-size: 11px; padding: $sp-1 $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
// 실사용 경로 적용: 실제로 배지를 골라도 확대·테두리는 트랜지션 없이 즉시 반영된다(색 자체는 JS 가 즉시 대입)
.bhs__badge { 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);
}
.bhs {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
max-width: 290px;
}
.bhs__badge {
appearance: none;
border: 2px solid transparent;
border-radius: 999px;
padding: 8px 12px;
font-weight: 700;
font-size: 12px;
color: #17141a;
cursor: pointer;
transition: transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1), border-color 220ms ease-out;
}
.bhs__badge:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
}
.bhs__badge[aria-checked=true] {
border-color: #17141a;
transform: scale(1.08);
}
@media (max-width: 340px) {
.bhs {
max-width: 250px;
}
.bhs__badge {
font-size: 11px;
padding: 4px 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.bhs__badge {
transition: none;
}
}(function () {
var LEVELS = ['簡単', '普通', '難しい', 'とても難しい', '最高'];
var BASE_HUE = 140, HUE_SPACING = 46, SAT = 45, LIGHT = 72;
var root = document.getElementById('bhs');
var badges = Array.prototype.slice.call(root.querySelectorAll('.bhs__badge'));
var selected = 2;
// 계산 겹: 배지 색을 고정 다섯 색이 아니라 단계 번호를 색상각 수식(기준 색상 + 번호*간격)에 넣어 구한다
function badgeHue(i) { return (BASE_HUE + i * HUE_SPACING) % 360; }
function badgeColor(i) { return 'hsl(' + badgeHue(i) + ', ' + SAT + '%, ' + LIGHT + '%)'; }
function paint() {
badges.forEach(function (b, i) {
b.style.background = badgeColor(i);
b.setAttribute('aria-checked', i === selected ? 'true' : 'false');
b.setAttribute('tabindex', i === selected ? '0' : '-1');
});
}
function human() { root.classList.remove('is-demo'); clearInterval(timer); }
function select(i) { selected = i; paint(); }
badges.forEach(function (b, i) {
b.addEventListener('click', function () { human(); select(i); });
b.addEventListener('keydown', function (e) {
if (e.key === 'ArrowRight') { e.preventDefault(); human(); var n = Math.min(LEVELS.length - 1, selected + 1); select(n); badges[n].focus({ preventScroll: true }); }
else if (e.key === 'ArrowLeft') { e.preventDefault(); human(); var p = Math.max(0, selected - 1); select(p); badges[p].focus({ preventScroll: true }); }
});
});
var sequence = [4, 0, 3, 1, 2];
var si = 0;
function tick() { select(sequence[si % sequence.length]); si++; }
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var timer = reduce ? null : setInterval(tick, 700);
paint();
}());import { useEffect, useRef } from 'react';
import styles from './DifficultySelectorBadgeHueShift.module.scss';
const LEVELS = ['쉬움', '보통', '어려움', '매우 어려움', '최고'];
const BASE_HUE = 140, HUE_SPACING = 46, SAT = 45, LIGHT = 72;
const SEQUENCE = [4, 0, 3, 1, 2];
const TICK_MS = 700;
export default function DifficultySelectorBadgeHueShift() {
const rootRef = useRef<HTMLDivElement>(null);
const badgeRefs = useRef<HTMLButtonElement[]>([]);
useEffect(() => {
const root = rootRef.current;
if (!root || !badgeRefs.current) return undefined;
const badges = badgeRefs.current;
if (badges.length !== LEVELS.length) return undefined;
let selected = 2;
// 계산 겹: 배지 색을 고정 다섯 색이 아니라 단계 번호를 색상각 수식(기준 색상 + 번호*간격)에 넣어 구한다
function badgeHue(i: number) { return (BASE_HUE + i * HUE_SPACING) % 360; }
function badgeColor(i: number) { return 'hsl(' + badgeHue(i) + ', ' + SAT + '%, ' + LIGHT + '%)'; }
function paint() {
badges.forEach((b, i) => {
b.style.background = badgeColor(i);
b.setAttribute('aria-checked', i === selected ? 'true' : 'false');
b.setAttribute('tabindex', i === selected ? '0' : '-1');
});
}
let timer = 0;
function human() { root!.classList.remove(styles['is-demo']); window.clearInterval(timer); }
function select(i: number) { selected = i; paint(); }
const offs = badges.map((b, i) => {
function onClick() { human(); select(i); }
function onKey(e: KeyboardEvent) {
if (e.key === 'ArrowRight') { e.preventDefault(); human(); const n = Math.min(LEVELS.length - 1, selected + 1); select(n); badges[n].focus({ preventScroll: true }); }
else if (e.key === 'ArrowLeft') { e.preventDefault(); human(); const p = Math.max(0, selected - 1); select(p); badges[p].focus({ preventScroll: true }); }
}
b.addEventListener('click', onClick);
b.addEventListener('keydown', onKey);
return () => { b.removeEventListener('click', onClick); b.removeEventListener('keydown', onKey); };
});
let si = 0;
function tick() { select(SEQUENCE[si % SEQUENCE.length]); si++; }
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) timer = window.setInterval(tick, TICK_MS);
paint();
return () => { offs.forEach((off) => off()); window.clearInterval(timer); };
}, []);
return (
<main className={styles.stage}>
<div className={styles.bhs + ' ' + styles['is-demo']} ref={rootRef} role="radiogroup" aria-label="난이도">
{LEVELS.map((label, i) => (
<button
key={label}
type="button"
className={styles.bhsBadge}
ref={(el: HTMLButtonElement | null) => { if (el && badgeRefs.current) badgeRefs.current[i] = el; }}
role="radio"
aria-checked={i === 2 ? 'true' : 'false'}
data-i={i}
tabIndex={i === 0 ? 0 : -1}
>
{label}
</button>
))}
</div>
</main>
);
}07 色が数式でつながるバッジ型難易度選択 — 計測 累積面積 4.045% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · 難易度選択 ui 9種 — 角度と数式で決まる選択まで
このデモが属する記事
難易度選択 ui 9種 — 角度と数式で決まる選択まで難易度選択 ui 9種のうち、1つは手を離すとスライダーのつまみが段階を少し行き過ぎてからばね計算で戻って止まり、1つは5つのバッジの色を固定値ではなく段階番号を入れた色相角の数式で計算し、1つは半円ゲージの針を引く指の角度を値に変換します この記事のデモをすべて見る


