처음부터 색상 선택기 웹 구성 요소 구축
Embed This Widget
Add the script tag and a data attribute to embed this widget.
Embed via iframe for maximum compatibility.
<iframe src="https://colorfyi.com/iframe/entity//" width="420" height="400" frameborder="0" style="border:0;border-radius:10px;max-width:100%" loading="lazy"></iframe>
Paste this URL in WordPress, Medium, or any oEmbed-compatible platform.
https://colorfyi.com/entity//
Add a dynamic SVG badge to your README or docs.
[](https://colorfyi.com/entity//)
Use the native HTML custom element.
기본 <input type="color">는 무딘 도구입니다. 플랫폼마다 다르게 보이고 스타일을 지정할 수 없으며 레이아웃에 대한 프로그래밍 방식 제어를 제공하지 않는 OS 색상 선택기에 위임합니다. 디자인 도구, 접근성 검사기 또는 색상 선택기가 중심 상호 작용인 제품의 경우 자체적으로 구축해야 합니다.
웹 구성 요소는 색상 선택기에 적합한 아키텍처입니다. 프레임워크에 구애받지 않고 Shadow DOM으로 내부 상태를 캡슐화하고, 표준 DOM 이벤트를 실행하고, 조정 없이 React, Vue, Angular 또는 일반 HTML에서 작업합니다. 한 번 구축하면 어디에서나 사용할 수 있습니다.
이 가이드는 2D 색상 영역 캔버스, 색조 슬라이더, 알파 채널 슬라이더, 16진수 입력 및 ARIA 레이블이 있는 전체 키보드 탐색 등 완전한 색상 선택기를 구현합니다.
UI를 위한 웹 구성요소 기초
맞춤 요소 API
웹 구성 요소는 HTMLElement를 확장하는 클래스입니다. 브라우저는 이를 태그 이름으로 등록하고 속성, 속성, 이벤트 및 슬롯 하위 항목에 대한 지원을 포함하여 모든 내장 요소처럼 작동합니다.
class ColorPicker extends HTMLElement {
// Called when element is first connected to the DOM
connectedCallback() {}
// Called when element is removed from the DOM
disconnectedCallback() {}
// Observed attributes trigger attributeChangedCallback
static get observedAttributes() {
return ['value', 'alpha'];
}
// Called when an observed attribute changes
attributeChangedCallback(name, oldValue, newValue) {}
}
customElements.define('color-picker', ColorPicker);
HTML의 어느 곳에서나 사용:
<color-picker value="#FF5733" alpha="true"></color-picker>
또는 React에서(이벤트 처리에 대한 참조 포함):
import { useRef, useEffect } from 'react';
function App() {
const pickerRef = useRef(null);
useEffect(() => {
const picker = pickerRef.current;
const handler = (e) => console.log(e.detail.hex);
picker.addEventListener('color-change', handler);
return () => picker.removeEventListener('color-change', handler);
}, []);
return <color-picker ref={pickerRef} value="#FF5733" />;
}
캡슐화를 위한 Shadow DOM
Shadow DOM은 구성 요소에 기본 문서와 격리된 비공개 DOM 트리를 제공합니다. 외부 CSS는 실수로 내부 요소의 스타일을 변경할 수 없으며 내부 ID는 페이지 ID와 충돌하지 않습니다.
class ColorPicker extends HTMLElement {
constructor() {
super();
// Attach a shadow root — 'open' allows external JS access via element.shadowRoot
this.shadow = this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadow.innerHTML = `
<style>
:host {
display: inline-block;
width: 280px;
font-family: system-ui, -apple-system, sans-serif;
}
.picker-area {
position: relative;
width: 100%;
height: 200px;
border-radius: 8px;
overflow: hidden;
cursor: crosshair;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
<div class="picker-area">
<canvas id="color-area" width="280" height="200"></canvas>
<div class="picker-cursor" role="presentation"></div>
</div>
<div class="sliders">
<div class="slider-row">
<canvas id="hue-slider" width="280" height="20" role="slider"
aria-label="Hue" aria-valuemin="0" aria-valuemax="360"></canvas>
</div>
</div>
`;
}
}
캔버스 기반 색상 영역 렌더링
색상 영역
기본 색상 선택기 영역은 2D 그라데이션입니다. 가로 축은 채도(0% ~ 100%)이고 세로 축은 값/밝기(상단 100%, 하단 0%)입니다. 색조는 색조 슬라이더로 고정됩니다. 이는 밝기를 다르게 매핑하는 HSL과 구별되는 HSV 색상 모델입니다.
function drawColorArea(canvas, hue) {
const ctx = canvas.getContext('2d');
const { width, height } = canvas;
// Draw the base hue as a horizontal white-to-hue gradient
const hueGradient = ctx.createLinearGradient(0, 0, width, 0);
hueGradient.addColorStop(0, 'white');
hueGradient.addColorStop(1, `hsl(${hue}, 100%, 50%)`);
ctx.fillStyle = hueGradient;
ctx.fillRect(0, 0, width, height);
// Overlay a vertical transparent-to-black gradient
const darkGradient = ctx.createLinearGradient(0, 0, 0, height);
darkGradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
darkGradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
ctx.fillStyle = darkGradient;
ctx.fillRect(0, 0, width, height);
}
흰색에서 색조로의 수평 그라데이션과 투명에서 검정색으로의 수직 그라데이션을 결합하면 해당 색조에 대한 완전한 HSV 공간이 생성됩니다. 왼쪽 위 모서리는 흰색(S=0, V=100)이고 오른쪽 위는 순수한 색상(S=100, V=100), 오른쪽 아래는 검은색(모든 S, V=0), 왼쪽 아래도 검은색입니다.
캔버스에서 픽셀 색상 읽기
사용자가 색상 영역을 클릭하거나 드래그하면 getImageData를 사용하여 해당 위치의 픽셀을 읽습니다.
function getColorAtPosition(canvas, x, y) {
const ctx = canvas.getContext('2d');
const pixel = ctx.getImageData(
Math.max(0, Math.min(x, canvas.width - 1)),
Math.max(0, Math.min(y, canvas.height - 1)),
1, 1
).data;
return {
r: pixel[0],
g: pixel[1],
b: pixel[2],
};
}
커서 그리기
캔버스 위에 겹쳐진 커서는 현재 선택 지점을 나타냅니다. 절대 위치에 있는 원으로 그립니다.
.picker-cursor {
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.3);
transform: translate(-50%, -50%);
pointer-events: none;
}
left 및 top를 캔버스 크기의 백분율로 설정하여 배치합니다.
function updateCursorPosition(cursor, x, y, canvasWidth, canvasHeight) {
cursor.style.left = `${(x / canvasWidth) * 100}%`;
cursor.style.top = `${(y / canvasHeight) * 100}%`;
}
Hue 및 Alpha Slider 구현
색조 슬라이더
색조 슬라이더는 0°에서 360°까지의 전체 무지개 스펙트럼으로 채워진 수평 캔버스입니다.
function drawHueSlider(canvas) {
const ctx = canvas.getContext('2d');
const { width, height } = canvas;
const gradient = ctx.createLinearGradient(0, 0, width, 0);
gradient.addColorStop(0, 'hsl(0, 100%, 50%)');
gradient.addColorStop(0.167, 'hsl(60, 100%, 50%)');
gradient.addColorStop(0.333, 'hsl(120, 100%, 50%)');
gradient.addColorStop(0.5, 'hsl(180, 100%, 50%)');
gradient.addColorStop(0.667, 'hsl(240, 100%, 50%)');
gradient.addColorStop(0.833, 'hsl(300, 100%, 50%)');
gradient.addColorStop(1, 'hsl(360, 100%, 50%)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
function hueFromX(x, canvasWidth) {
return Math.round((x / canvasWidth) * 360);
}
function xFromHue(hue, canvasWidth) {
return (hue / 360) * canvasWidth;
}
알파 슬라이더
알파 슬라이더에는 색상-투명 그라데이션 아래에 체커보드 배경(투명도에 대한 표준 표시기)이 필요합니다.
function drawAlphaSlider(canvas, hex) {
const ctx = canvas.getContext('2d');
const { width, height } = canvas;
// Checkerboard pattern via CSS background on the container,
// or drawn manually for canvas-only:
const size = 6;
for (let row = 0; row < Math.ceil(height / size); row++) {
for (let col = 0; col < Math.ceil(width / size); col++) {
ctx.fillStyle = (row + col) % 2 === 0 ? '#CCCCCC' : '#FFFFFF';
ctx.fillRect(col * size, row * size, size, size);
}
}
// Overlay: color to transparent gradient
const gradient = ctx.createLinearGradient(0, 0, width, 0);
gradient.addColorStop(0, `${hex}00`); // transparent
gradient.addColorStop(1, hex); // opaque
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
통합된 슬라이더 상호작용
두 슬라이더 모두 동일한 드래그 상호 작용 패턴을 공유합니다. 재사용 가능한 도우미로 추출합니다.
function makeDraggable(element, onMove) {
let dragging = false;
const getX = (e) => {
const rect = element.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
return Math.max(0, Math.min(clientX - rect.left, rect.width));
};
element.addEventListener('mousedown', (e) => {
dragging = true;
onMove(getX(e));
});
element.addEventListener('touchstart', (e) => {
dragging = true;
onMove(getX(e));
}, { passive: true });
document.addEventListener('mousemove', (e) => {
if (dragging) onMove(getX(e));
});
document.addEventListener('touchmove', (e) => {
if (dragging) onMove(getX(e));
}, { passive: true });
document.addEventListener('mouseup', () => { dragging = false; });
document.addEventListener('touchend', () => { dragging = false; });
}
document 수준 mousemove 및 mouseup 핸들러는 커서가 슬라이더 범위 밖으로 이동하더라도 드래그가 계속되도록 보장합니다. 이는 유용성에 매우 중요합니다.
색상 변경 이벤트 전달
사용자 정의 이벤트 패턴
풀 컬러 데이터가 포함된 detail 페이로드와 함께 CustomEvent를 사용하십시오. 충돌을 피하기 위해 네임스페이스 접두사를 사용하여 이름을 지정합니다.
function dispatchColorChange(element, color) {
const event = new CustomEvent('color-change', {
bubbles: true, // Propagates up the DOM tree
composed: true, // Crosses Shadow DOM boundaries
detail: {
hex: color.hex,
rgb: color.rgb,
hsl: color.hsl,
alpha: color.alpha,
hexWithAlpha: color.alpha < 1
? `${color.hex}${Math.round(color.alpha * 255).toString(16).padStart(2, '0').toUpperCase()}`
: color.hex,
},
});
element.dispatchEvent(event);
}
composed: true는 필수적입니다. composed: true가 없으면 Shadow DOM 내부에서 전달된 이벤트가 경계를 넘지 않으며 기본 문서의 코드에 표시되지 않습니다.
내부 상태 관리
색상 상태를 HSV + 알파(시각적 선택기의 가장 자연스러운 모델)로 유지하고 출력 시 다른 형식으로 변환합니다.
class ColorPicker extends HTMLElement {
#state = {
h: 0, // Hue 0-360
s: 100, // Saturation 0-100 (HSV)
v: 100, // Value/Brightness 0-100 (HSV)
a: 1, // Alpha 0-1
};
get #hex() {
const { r, g, b } = hsvToRgb(this.#state.h, this.#state.s, this.#state.v);
return `#${[r, g, b].map(v => v.toString(16).padStart(2, '0')).join('').toUpperCase()}`;
}
#update(changes) {
this.#state = { ...this.#state, ...changes };
this.#render();
dispatchColorChange(this, {
hex: this.#hex,
rgb: hsvToRgb(this.#state.h, this.#state.s, this.#state.v),
hsl: rgbToHsl(hsvToRgb(this.#state.h, this.#state.s, this.#state.v)),
alpha: this.#state.a,
});
}
}
#update 메서드는 색상 슬라이더를 끌거나, 색상 영역을 클릭하거나, 16진수 입력을 입력하는 등 색상의 일부가 변경될 때마다 호출됩니다. 상태를 업데이트하고, 영향을 받은 캔버스를 다시 렌더링하고, 이벤트를 시작합니다.
16진수 입력 동기화
16진수 입력은 현재 색상을 반영해야 하며 사용자 편집도 허용해야 합니다.
setupHexInput(hexInput) {
// Reflect state into input on every update
this.addEventListener('_internal-state-change', () => {
if (document.activeElement !== hexInput) {
hexInput.value = this.#hex;
}
});
// Parse input and update state when user types
hexInput.addEventListener('input', (e) => {
const raw = e.target.value.trim();
const match = raw.match(/^#?([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$/);
if (!match) return; // Silently ignore incomplete input
const full = match[1].length === 3
? match[1].split('').map(c => c + c).join('')
: match[1];
const r = parseInt(full.slice(0, 2), 16);
const g = parseInt(full.slice(2, 4), 16);
const b = parseInt(full.slice(4, 6), 16);
const { h, s, v } = rgbToHsv(r, g, b);
this.#update({ h, s, v });
});
}
입력에 초점이 맞춰졌을 때 업데이트를 건너뛰면 #FF를 입력하면 16진수 입력 중간 유형을 덮어쓰는 상태 업데이트가 발생하는 피드백 루프가 방지됩니다.
접근성: 키보드 탐색, ARIA 라벨, 화면 판독기
접근성 문제
캔버스 요소는 화면 판독기에 불투명합니다. getImageData는 픽셀 값을 반환하지만 해당 픽셀은 보조 기술에 의미론적 의미를 전달하지 않습니다. 색상 선택기에 액세스할 수 있도록 하려면 시각적 캔버스 위에 의미 체계 정보를 계층화해야 합니다.
슬라이더의 ARIA 역할
색상 영역, 색조 슬라이더 및 알파 슬라이더는 모두 2차원 슬라이더로 작동합니다. 전체 ARIA 슬라이더 속성이 설정된 role="slider"를 사용하세요.
<!-- Color area: 2D, use two separate sliders for the axes -->
<div
role="slider"
id="color-area-saturation"
aria-label="Color saturation"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="75"
aria-valuetext="75% saturation"
tabindex="0"
></div>
<div
role="slider"
id="color-area-brightness"
aria-label="Color brightness"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="90"
aria-valuetext="90% brightness"
tabindex="0"
></div>
<!-- Hue slider -->
<div
role="slider"
id="hue-slider"
aria-label="Hue"
aria-valuemin="0"
aria-valuemax="360"
aria-valuenow="120"
aria-valuetext="120 degrees, green"
tabindex="0"
></div>
#update 방법에서 aria-valuenow 및 aria-valuetext를 업데이트합니다.
#updateAriaAttributes() {
const hueEl = this.shadow.querySelector('#hue-slider');
hueEl.setAttribute('aria-valuenow', this.#state.h);
hueEl.setAttribute('aria-valuetext', `${this.#state.h} degrees`);
const satEl = this.shadow.querySelector('#color-area-saturation');
satEl.setAttribute('aria-valuenow', Math.round(this.#state.s));
satEl.setAttribute('aria-valuetext', `${Math.round(this.#state.s)}% saturation`);
}
키보드 탐색
각 슬라이더 요소는 키보드 사용자의 화살표 키에 반응해야 합니다. 표준 슬라이더 키보드 상호 작용은 다음과 같습니다. - 오른쪽 화살표 / 위쪽 화살표: 값을 1단위 증가(또는 Shift를 사용하여 더 거친 단계의 경우 5) - 왼쪽 화살표 / 아래쪽 화살표 : 값 감소 - 홈 : 최소로 설정 - 종료 : 최대로 설정
setupKeyboardNavigation(hueSlider) {
hueSlider.addEventListener('keydown', (e) => {
let delta = 0;
const step = e.shiftKey ? 10 : 1;
switch (e.key) {
case 'ArrowRight':
case 'ArrowUp':
delta = step;
break;
case 'ArrowLeft':
case 'ArrowDown':
delta = -step;
break;
case 'Home':
this.#update({ h: 0 });
e.preventDefault();
return;
case 'End':
this.#update({ h: 360 });
e.preventDefault();
return;
default:
return;
}
e.preventDefault();
this.#update({ h: Math.max(0, Math.min(360, this.#state.h + delta)) });
});
}
2D 색상 영역의 경우 4개의 화살표 키를 모두 처리하여 두 차원에서 이동합니다.
setupColorAreaKeyboard(colorArea) {
colorArea.addEventListener('keydown', (e) => {
const step = e.shiftKey ? 5 : 1;
let { s, v } = this.#state;
switch (e.key) {
case 'ArrowRight': s = Math.min(100, s + step); break;
case 'ArrowLeft': s = Math.max(0, s - step); break;
case 'ArrowUp': v = Math.min(100, v + step); break;
case 'ArrowDown': v = Math.max(0, v - step); break;
default: return;
}
e.preventDefault();
this.#update({ s, v });
});
}
스크린 리더 공지사항을 위한 라이브 영역
사용자가 선택기와 상호작용할 때 화면 판독기가 선택한 색상을 알리도록 하려면 ARIA 라이브 영역을 추가하세요.
<div
role="status"
aria-live="polite"
aria-atomic="true"
class="sr-only"
id="color-announcement"
></div>
색상이 변경될 때마다 업데이트하세요.
#announceColor() {
const announcement = this.shadow.querySelector('#color-announcement');
const { h, s, v, a } = this.#state;
announcement.textContent = `Selected color: ${this.#hex}, ` +
`hue ${h} degrees, saturation ${Math.round(s)}%, ` +
`brightness ${Math.round(v)}%` +
(a < 1 ? `, ${Math.round(a * 100)}% opacity` : '');
}
aria-live="polite" 설정은 사용자가 다른 요소와 적극적으로 상호 작용하지 않을 때 변경 사항을 알려줍니다. 사용자가 드래그할 가능성이 있는 색상 선택기의 경우 polite가 맞습니다. assertive는 다음 알림으로 모든 알림을 중단합니다.
초점 표시기
Shadow DOM 내부에 포커스 링이 표시되는지 확인하세요. :focus-visible 의사 클래스는 마우스 상호 작용을 방해하지 않고 키보드 전용 초점 링을 제공합니다.
/* Inside the Shadow DOM <style> */
[role="slider"]:focus-visible {
outline: 2px solid #2563EB;
outline-offset: 2px;
}
.hex-input:focus-visible {
outline: 2px solid #2563EB;
outline-offset: 1px;
border-radius: 4px;
}
완전한 접근 가능한 구성요소
이 모든 것을 종합하면 구성 요소의 connectedCallback는 시각적 상호 작용이 발생하기 전에 ARIA 역할 및 키보드 핸들러를 사용하여 모든 대화형 요소를 설정합니다.
connectedCallback() {
this.shadow.innerHTML = this.#template();
this.#setupColorArea();
this.#setupHueSlider();
this.#setupAlphaSlider();
this.#setupHexInput();
this.#setupKeyboardNavigation();
this.#render();
}
#template() {
return `
<style>${this.#css()}</style>
<div class="color-picker" role="group" aria-label="Color picker">
<!-- 2D Color Area -->
<div class="picker-area-wrapper">
<canvas id="color-area" width="280" height="200"
aria-hidden="true"></canvas>
<div class="cursor" role="presentation"></div>
<div id="color-area-interactive"
role="slider"
aria-label="Color saturation and brightness"
aria-valuemin="0" aria-valuemax="100"
aria-valuenow="100" aria-valuetext="full saturation, full brightness"
tabindex="0"
class="interactive-overlay">
</div>
</div>
<!-- Hue Slider -->
<div class="slider-wrapper">
<canvas id="hue-canvas" width="280" height="20" aria-hidden="true"></canvas>
<div id="hue-thumb" class="slider-thumb" role="slider"
aria-label="Hue" aria-valuemin="0" aria-valuemax="360"
aria-valuenow="0" aria-valuetext="0 degrees, red"
tabindex="0"></div>
</div>
<!-- Alpha Slider -->
<div class="slider-wrapper" id="alpha-wrapper">
<canvas id="alpha-canvas" width="280" height="20" aria-hidden="true"></canvas>
<div id="alpha-thumb" class="slider-thumb" role="slider"
aria-label="Opacity" aria-valuemin="0" aria-valuemax="100"
aria-valuenow="100" aria-valuetext="100% opacity"
tabindex="0"></div>
</div>
<!-- Hex Input -->
<div class="hex-row">
<label for="hex-input" class="hex-label">HEX</label>
<input id="hex-input" type="text" class="hex-input"
value="#FF5733" maxlength="7"
aria-label="Hex color code"
autocomplete="off" spellcheck="false" />
</div>
<!-- Screen reader live region -->
<div role="status" aria-live="polite" aria-atomic="true"
id="color-announcement" class="sr-only"></div>
</div>
`;
}
캔버스의 aria-hidden="true"는 스크린 리더에게 픽셀 콘텐츠를 완전히 무시하라고 지시합니다. 의미론적 의미는 옆에 있는 대화형 오버레이 요소에 의해 제공됩니다.
구성 요소 게시 및 사용
npm에 게시
웹 구성요소는 단지 JavaScript 모듈일 뿐입니다. 최소한의 package.json로 게시하세요.
{
"name": "@yourscope/color-picker",
"version": "1.0.0",
"type": "module",
"main": "./dist/color-picker.js",
"exports": {
".": "./dist/color-picker.js"
},
"files": ["dist/"]
}
어떤 프로젝트에서든 가져오기:
import '@yourscope/color-picker';
// Now <color-picker> is registered globally
ColorFYI 변환기와 함께 사용
색상 선택기를 색상 변환기 API와 결합하여 시각적 선택기와 함께 전체 색상 데이터를 표시합니다.
document.querySelector('color-picker').addEventListener('color-change', async (e) => {
const hex = e.detail.hex.replace('#', '');
const response = await fetch(`/api/color/${hex}`);
const data = await response.json();
document.querySelector('#hex-display').textContent = data.hex;
document.querySelector('#rgb-display').textContent = data.rgb.css;
document.querySelector('#hsl-display').textContent = data.hsl.css;
document.querySelector('#oklch-display').textContent = data.oklch.css;
});
그리고 팔레트 생성기를 사용하여 선택한 값에 대해 조화로운 색상을 표시하여 선택기를 중심으로 완전한 색상 탐색 경험을 구축합니다.
주요 내용
- Shadow DOM이 포함된 웹 구성 요소는 프레임워크에 구애받지 않고 스타일이 캡슐화되어 있으며 표준 DOM 이벤트를 실행합니다. 이는 색상 선택기와 같은 재사용 가능한 UI에 적합한 아키텍처입니다.
- 색상 영역은 두 개의 중첩된 캔버스 그라데이션을 사용합니다. 채도는 수평 흰색에서 색조로, 수직은 투명에서 검정색으로 값/밝기를 나타냅니다.
getImageData는 드래그 위치에서 픽셀 색상을 읽습니다. - 색조 및 알파 슬라이더는 드래그 가능한 동일한 캔버스 패턴을 사용합니다. 키는 요소가 아닌
mousemove및mouseup를document에 연결하므로 커서가 슬라이더를 벗어나도 드래그가 작동합니다. - 이벤트가 Shadow DOM 경계를 넘어 상위 문서 코드에 표시되도록
composed: true와 함께CustomEvent를 디스패치합니다. - 캔버스 요소는 화면 판독기에 불투명합니다. 상태가 변경될 때마다 업데이트되는
aria-valuenow및aria-valuetext가 포함된role="slider"오버레이 요소를 통해 의미론적 의미를 제공합니다. - 각 슬라이더에 전체 화살표 키 탐색 구현: 화살표 키의 단일 단계, Shift를 사용한 5단계, 최소/최대의 홈/끝.
- 각 변경 사항에 대한 전체 색상 설명(16진수, 색조, 채도, 밝기, 불투명도)을 알리는
aria-live="polite"상태 영역을 추가합니다. - 색상 변환기 API와 결합하여 전체 형식 분석을 표시하고 팔레트 생성기를 사용하여 선택한 값과 함께 조화로운 색상을 표시합니다.