04市松模様の上で透明度を動かすアルファスライダー
市松模様を敷いた帯に色を重ね、つまみを動かすと、透けて見える分だけアルファ値が下がります。
使いどころオーバーレイ色の決定、半透明背景トークン
repeating-conic-gradientaria-valuetextcounter-set

<main class="stage">
<div class="al is-demo">
<p class="al__head" aria-hidden="true">
<span class="al__cap">透明度</span><span class="al__val"></span>
</p>
<div class="al__track" id="alphaTrack" role="slider" tabindex="0"
aria-label="透明度を選ぶ"
aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"
aria-valuetext="透明度 100%">
<span class="al__knob" aria-hidden="true"></span>
</div>
<p class="al__note" aria-hidden="true">市松模様の上のプレビュー</p>
<div class="al__preview" aria-hidden="true"><span class="al__fill"></span></div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 알파 슬라이더 — 아래층에 칸무늬를 깔고 그 위에 색을 덮는다. 손잡이를 왼쪽으로 밀수록
// 색이 옅어져 뒤 칸무늬가 드러나고, 무늬를 가린 만큼이 곧 알파 값이다(--a 가 그대로 덮개의 불투명도다).
// 움직이는 값은 --a(0~100) 하나뿐이다. 손잡이 위치도, 미리보기 사각형의 불투명도도,
// 옆의 숫자도 전부 그 한 값에서 나온다 — 숫자는 counter-set 이 계수기로 옮겨 찍으므로
// JS 가 글자를 따로 쓰지 않고, 어느 프레임에서도 손잡이 자리와 숫자가 어긋나지 않는다.
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
@property --a { syntax: "<integer>"; inherits: true; initial-value: 100; }
.al {
box-sizing: border-box;
width: 288px;
max-width: 100%;
padding: $sp-3;
border-radius: $r-card;
background: $subject-cream;
color: $subject-ink;
font-family: inherit;
}
.al * { box-sizing: border-box; }
.al__head {
display: flex;
align-items: baseline;
gap: $sp-2;
margin: 0 0 $sp-2;
}
.al__cap {
font-size: 11px;
font-weight: 500;
line-height: 1.2;
color: rgba(23, 20, 26, .62);
}
// 숫자는 JS 가 쓰지 않는다 — counter-set 이 --a 를 계수기로 옮기고 ::after 가 찍는다
.al__val {
counter-set: ca calc(var(--a));
font-size: 13px;
font-weight: 700;
line-height: 1.2;
letter-spacing: .04em;
}
.al__val::after { content: counter(ca) "%"; }
.al__track {
position: relative;
width: 264px; // 카드 안쪽 폭 그대로 — 1% 가 2.64px 이다
max-width: 100%;
height: 20px;
border-radius: $r-pill;
cursor: pointer;
touch-action: none;
background-color: $stage-paper;
// 위층은 투명 → 불투명 색 덮개, 아래층은 12px 타일 칸무늬(타일 안이 네 칸이라 한 칸은 6px)
background-image:
linear-gradient(to right, rgba($subject-blue, 0), $subject-blue),
repeating-conic-gradient(#d8d4cc 0 25%, $stage-paper 0 50%);
background-size: auto, 12px 12px;
}
.al__track:focus-visible { outline: 3px solid $color; outline-offset: 3px; }
.al__knob {
position: absolute;
left: 0;
top: 50%;
width: 18px;
height: 18px;
border-radius: 50%;
background: $stage-paper;
border: 2px solid $subject-ink;
box-shadow: $shadow-press;
// 1% 는 2.64px. 자리로 옮긴 뒤 자기 크기의 절반만큼 되밀어 중심을 그 자리에 놓는다
transform: translateX(calc(var(--a) * 2.64px)) translate(-50%, -50%);
}
.al__note {
margin: $sp-2 0 $sp-1;
font-size: 11px;
font-weight: 500;
line-height: 1.2;
color: rgba(23, 20, 26, .62);
}
.al__preview {
position: relative;
height: 52px;
border-radius: $r-sm;
overflow: hidden;
background-color: $stage-paper;
background-image: repeating-conic-gradient(#d8d4cc 0 25%, $stage-paper 0 50%);
background-size: 12px 12px;
}
.al__fill {
position: absolute;
inset: 0;
background: $subject-blue;
// 알파를 색 안에 넣지 않고 덮개의 불투명도로 준다 — 값이 --a 한 곳에서만 온다
opacity: calc(var(--a) * 1%);
}
// ── 미리보기 루프(정적 규칙 먼저, 그 위에 덧씌움) ─────────────────
.al.is-demo {
animation-name: alSlide;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
// 100% → 24% → 같은 길을 되짚어 100%. 90% 에 제자리로 돌아온다
// (24프레임의 마지막 장이 95.8% 지점이라 그 뒤에 두면 복귀가 영영 안 찍힌다)
@keyframes alSlide {
0% { --a: 100; }
45% { --a: 24; }
90%, 100% { --a: 100; }
}
@media (prefers-reduced-motion: reduce) {
.al.is-demo { animation: none !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);
}
@property --a {
syntax: "<integer>";
inherits: true;
initial-value: 100;
}
.al {
box-sizing: border-box;
width: 288px;
max-width: 100%;
padding: 12px;
border-radius: 16px;
background: #fff7e6;
color: #17141a;
font-family: inherit;
}
.al * {
box-sizing: border-box;
}
.al__head {
display: flex;
align-items: baseline;
gap: 8px;
margin: 0 0 8px;
}
.al__cap {
font-size: 11px;
font-weight: 500;
line-height: 1.2;
color: rgba(23, 20, 26, 0.62);
}
.al__val {
counter-set: ca calc(var(--a));
font-size: 13px;
font-weight: 700;
line-height: 1.2;
letter-spacing: 0.04em;
}
.al__val::after {
content: counter(ca) "%";
}
.al__track {
position: relative;
width: 264px;
max-width: 100%;
height: 20px;
border-radius: 999px;
cursor: pointer;
touch-action: none;
background-color: #ffffff;
background-image: linear-gradient(to right, rgba(47, 109, 246, 0), #2f6df6), repeating-conic-gradient(#d8d4cc 0 25%, #ffffff 0 50%);
background-size: auto, 12px 12px;
}
.al__track:focus-visible {
outline: 3px solid #2f6df6;
outline-offset: 3px;
}
.al__knob {
position: absolute;
left: 0;
top: 50%;
width: 18px;
height: 18px;
border-radius: 50%;
background: #ffffff;
border: 2px solid #17141a;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
transform: translateX(calc(var(--a) * 2.64px)) translate(-50%, -50%);
}
.al__note {
margin: 8px 0 4px;
font-size: 11px;
font-weight: 500;
line-height: 1.2;
color: rgba(23, 20, 26, 0.62);
}
.al__preview {
position: relative;
height: 52px;
border-radius: 8px;
overflow: hidden;
background-color: #ffffff;
background-image: repeating-conic-gradient(#d8d4cc 0 25%, #ffffff 0 50%);
background-size: 12px 12px;
}
.al__fill {
position: absolute;
inset: 0;
background: #2f6df6;
opacity: calc(var(--a) * 1%);
}
.al.is-demo {
animation-name: alSlide;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
@keyframes alSlide {
0% {
--a: 100;
}
45% {
--a: 24;
}
90%, 100% {
--a: 100;
}
}
@media (prefers-reduced-motion: reduce) {
.al.is-demo {
animation: none !important;
}
}(function () {
var root = document.querySelector('.al');
var track = document.getElementById('alphaTrack');
var alphaFormat = '透明度 %a%';
var alpha = 100;
// 값은 --a 하나뿐이다. 손잡이 위치도, 미리보기 사각형의 불투명도도, 옆의 숫자도 전부 이 한 값에서 나온다
function applyAlpha() {
root.style.setProperty('--a', alpha);
track.setAttribute('aria-valuenow', alpha);
track.setAttribute('aria-valuetext', alphaFormat.replace('%a', alpha));
}
function pickAlpha(e) {
var box = track.getBoundingClientRect();
var t = Math.min(1, Math.max(0, (e.clientX - box.left) / box.width));
alpha = Math.round(t * 100);
applyAlpha();
}
track.addEventListener('pointerdown', function (e) {
root.classList.remove('is-demo');
track.setPointerCapture(e.pointerId); // 띠 밖으로 끌어도 이 요소가 계속 포인터를 받는다
pickAlpha(e);
});
track.addEventListener('pointermove', function (e) {
if (track.hasPointerCapture(e.pointerId)) { pickAlpha(e); }
});
track.addEventListener('keydown', function (e) {
var step = e.shiftKey ? 10 : 2;
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { alpha = Math.min(100, alpha + step); }
else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { alpha = Math.max(0, alpha - step); }
else { return; }
e.preventDefault();
root.classList.remove('is-demo');
applyAlpha();
});
applyAlpha();
})();// AlphaCheckerboard — vanilla 판(run/286/demo/04-color-picker-alpha-checkerboard) 의 React 포트.
// 변수 3개(duration·easing·color)는 props 로 받아 CSS 변수(var(--duration) 등)로 들어온다.
// 상태는 alpha 숫자 하나뿐이다. 손잡이 위치도, 미리보기 사각형의 불투명도도, 옆의 숫자도
// 전부 --a 한 값에서 나오고 숫자는 module.scss 의 counter-set 이 찍는다 — JS 가 글자를 쓰지 않는다.
import { useState } from "react";
import type { CSSProperties, PointerEvent as ReactPointerEvent } from "react";
import styles from "./AlphaCheckerboard.module.scss";
type KeyEvt = { key: string; shiftKey: boolean; preventDefault(): void };
export type AlphaCheckerboardProps = {
trackLabel?: string;
capLabel?: string;
noteLabel?: string;
/** "투명도 %a%" 처럼 자리표시자 하나를 가진 틀 */
alphaFormat?: string;
/** 격자 미리보기용 2초 자동 루프. 사람이 손대면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
"--a"?: number;
};
export default function AlphaCheckerboard({
trackLabel = "투명도 고르기",
capLabel = "투명도",
noteLabel = "칸무늬 위 미리보기",
alphaFormat = "투명도 %a%",
demo = true,
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#2f6df6",
}: AlphaCheckerboardProps) {
const [live, setLive] = useState(demo);
const [alpha, setAlpha] = useState(100);
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
"--a": alpha,
};
function alphaAt(e: ReactPointerEvent<HTMLDivElement>): number {
const box = e.currentTarget.getBoundingClientRect();
const t = Math.min(1, Math.max(0, (e.clientX - box.left) / box.width));
return Math.round(t * 100);
}
function start(e: ReactPointerEvent<HTMLDivElement>) {
setLive(false);
e.currentTarget.setPointerCapture(e.pointerId); // 띠 밖으로 끌어도 계속 포인터를 받는다
setAlpha(alphaAt(e));
}
function drag(e: ReactPointerEvent<HTMLDivElement>) {
if (e.currentTarget.hasPointerCapture(e.pointerId)) setAlpha(alphaAt(e));
}
function press(e: KeyEvt) {
const step = e.shiftKey ? 10 : 2;
if (e.key === "ArrowRight" || e.key === "ArrowUp") setAlpha(Math.min(100, alpha + step));
else if (e.key === "ArrowLeft" || e.key === "ArrowDown") setAlpha(Math.max(0, alpha - step));
else return;
e.preventDefault();
setLive(false);
}
const alphaText = alphaFormat.replace("%a", String(alpha));
return (
<div className={[styles.al, live ? styles.isDemo : ""].filter(Boolean).join(" ")} style={vars}>
<p className={styles.head} aria-hidden="true">
<span className={styles.cap}>{capLabel}</span>
<span className={styles.val} />
</p>
<div
className={styles.track}
role="slider"
tabIndex={0}
aria-label={trackLabel}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={alpha}
aria-valuetext={alphaText}
onPointerDown={start}
onPointerMove={drag}
onKeyDown={press}
>
<span className={styles.knob} aria-hidden="true" />
</div>
<p className={styles.note} aria-hidden="true">
{noteLabel}
</p>
<div className={styles.preview} aria-hidden="true">
<span className={styles.fill} />
</div>
</div>
);
}04 市松模様の上で透明度を動かすアルファスライダー — 計測 累積面積 13.06% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · カラーピッカー UI 9選 — ライブラリなしで作る色選択
このデモが属する記事
カラーピッカー UI 9選 — ライブラリなしで作る色選択カラーピッカー ui とは、画面上で色を選んで HEX コードとして返す選択 UI のことです。このページにはその部品が 9 種、ライブラリなしで色空間の計算まで手書きしたコードが実際に動くまま入っています。彩度・明度の四角形、カラーホイー この記事のデモをすべて見る


