튜토리얼

처음부터 색상 선택기 웹 구성 요소 구축

10분 읽기

기본 <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;
}

lefttop를 캔버스 크기의 백분율로 설정하여 배치합니다.

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 수준 mousemovemouseup 핸들러는 커서가 슬라이더 범위 밖으로 이동하더라도 드래그가 계속되도록 보장합니다. 이는 유용성에 매우 중요합니다.


색상 변경 이벤트 전달

사용자 정의 이벤트 패턴

풀 컬러 데이터가 포함된 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-valuenowaria-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는 드래그 위치에서 픽셀 색상을 읽습니다.
  • 색조 및 알파 슬라이더는 드래그 가능한 동일한 캔버스 패턴을 사용합니다. 키는 요소가 아닌 mousemovemouseupdocument에 연결하므로 커서가 슬라이더를 벗어나도 드래그가 작동합니다.
  • 이벤트가 Shadow DOM 경계를 넘어 상위 문서 코드에 표시되도록 composed: true와 함께 CustomEvent를 디스패치합니다.
  • 캔버스 요소는 화면 판독기에 불투명합니다. 상태가 변경될 때마다 업데이트되는 aria-valuenowaria-valuetext가 포함된 role="slider" 오버레이 요소를 통해 의미론적 의미를 제공합니다.
  • 각 슬라이더에 전체 화살표 키 탐색 구현: 화살표 키의 단일 단계, Shift를 사용한 5단계, 최소/최대의 홈/끝.
  • 각 변경 사항에 대한 전체 색상 설명(16진수, 색조, 채도, 밝기, 불투명도)을 알리는 aria-live="polite" 상태 영역을 추가합니다.
  • 색상 변환기 API와 결합하여 전체 형식 분석을 표시하고 팔레트 생성기를 사용하여 선택한 값과 함께 조화로운 색상을 표시합니다.

관련 색상

관련 브랜드

관련 도구