
ウェブサイトで色を選択する UI を作成する場合、<input type="color">
を使用すると手軽に実装できます。しかし、この標準のカラーピッカーはブラウザごとに UI が異なり、デザインの統一が難しいという課題があります。
そこで、この記事では HTML, CSS, JavaScript を使って、カスタマイズ可能なカラーピッカーを自作する方法を紹介します。
目次
動作サンプル
まずは、実際に動作するサンプルです。
See the Pen Color picker using html, css, javascript by YUTSUZO (@YUTSUZO) on CodePen.
彩度(Saturation)と明度(Value)を選択するグラデーションのキャンバスと、色相(Hue)を選択するスライダーがあり、選択した色が 16 進数のカラーコードで出力されるようになっています。
サンプルのため、わかりやすいようにキャンバスの下のinput
を可視化していますが、hidden
でも問題ありません。
動作サンプルのコード
上記の動作サンプルのコードは以下のようになっています。各コードの重要な部分の解説については後述します。
HTML のコード
<div class="hsv-color-picker">
<!-- グラデーションのキャンバスと彩度(Saturation)、明度(Value)を保持する input -->
<div class="hsv-canvas-wrapper">
<canvas id="hsvCanvas"></canvas>
<div id="hsvCanvasThumb" class="thumb"></div>
<input id="hsvSaturationInput" type="text" value="0">
<input id="hsvValueInput" type="text" value="100">
</div>
<!-- 色相(Hue)のスライダー -->
<input id="hsvHueInput" type="range" name="hsv-hue" min="0" max="360" value="0">
<!-- 16 進数のカラーコードを表示する input -->
<input id="hexInput" type="text" name="hex" value="#FFFFFF">
</div>
CSS のコード
/* CSS 変数の定義 */
:root {
--track-width: 100%;
--track-height: 8px;
--track-border-width: 0;
--track-background: linear-gradient(to right, hsl(0deg 100% 50%), hsl(60deg 100% 50%), hsl(120deg 100% 50%), hsl(180deg 100% 50%), hsl(240deg 100% 50%), hsl(300deg 100% 50%), hsl(360deg 100% 50%));
--track-border-color: transparent;
--track-border-radius: 6px;
--thumb-width: 24px;
--thumb-height: 24px;
--thumb-color-picker: transparent;
--thumb-color-slider--hue: 0;
--thumb-color-slider: hsl(var(--thumb-color-slider--hue) 100 50);
--thumb-border-width: 2px;
--thumb-border-color: #FFFFFF;
--thumb-border-radius: 12px;
--thumb-shadow: -2px 2px 2px rgb(11 0 0 / 0.2);
}
/* input[type="text"] のリセット用スタイル */
input[type="text"] {
width: 100%;
box-sizing: border-box;
}
/* input[type="range"] のリセット用スタイル */
input[type="range"] {
appearance: none;
touch-action: none;
margin: 0;
background: transparent;
}
input[type="range"]::-webkit-slider-thumb {
appearance: none;
}
input[type="range"]::-moz-range-track,
input[type="range"]::-moz-range-thumb {
border: none;
box-sizing: border-box;
background: transparent;
}
/* body の余白 */
body {
padding: 20px;
}
/* ここからカラーピッカーのスタイル */
.hsv-color-picker {
margin-inline: auto;
width: 240px;
}
.hsv-canvas-wrapper {
position: relative;
margin-bottom: 8px;
touch-action: none;
}
.hsv-canvas-wrapper canvas {
display: block;
width: 100%;
aspect-ratio: 1 / 1;
}
.hsv-canvas-wrapper .thumb {
position: absolute;
top: 0;
left: 0;
width: var(--thumb-width);
height: var(--thumb-height);
border: solid var(--thumb-border-width) var(--thumb-border-color);
border-radius: var(--thumb-border-radius);
background: var(--thumb-color-picker);
box-shadow: var(--thumb-shadow);
box-sizing: border-box;
transform: translate(calc(var(--thumb-height) / 2 * -1), calc(var(--thumb-height) / 2 * -1));
}
/* ここからスライダーのスタイル */
input[type="range"][name="hsv-hue"] {
width: var(--track-width);
height: max(var(--thumb-height), var(--track-height));
}
input[type="range"][name="hsv-hue"]::-webkit-slider-runnable-track {
height: var(--track-height);
border: solid var(--track-border-width) var(--track-border-color);
border-radius: var(--track-border-radius);
background-image: var(--track-background);
}
input[type="range"][name="hsv-hue"]::-webkit-slider-thumb {
margin-top: calc((var(--thumb-height) - var(--track-height)) / 2 * -1);
width: var(--thumb-width);
height: var(--thumb-height);
border: solid var(--thumb-border-width) var(--thumb-border-color);
border-radius: var(--thumb-border-radius);
background: var(--thumb-color-slider);
box-shadow: var(--thumb-shadow);
}
input[type="range"][name="hsv-hue"]::-moz-range-track {
height: var(--track-height);
border: solid var(--track-border-width) var(--track-border-color);
border-radius: var(--track-border-radius);
background-image: var(--track-background);
}
input[type="range"][name="hsv-hue"]::-moz-range-thumb {
width: var(--thumb-width);
height: var(--thumb-height);
border: solid var(--thumb-border-width) var(--thumb-border-color);
border-radius: var(--thumb-border-radius);
background: var(--thumb-color-slider);
box-shadow: var(--thumb-shadow);
}
JavaScript のコード
window.addEventListener("DOMContentLoaded", function () {
// :root 疑似クラスを取得
const root = document.querySelector(":root");
// hsvCanvas のサイズ取得
const pickerWidth = hsvCanvas.getBoundingClientRect().width;
const pickerHeight = hsvCanvas.getBoundingClientRect().height;
// 初期設定
hsvCanvasThumb.style.left = (pickerWidth * hsvSaturationInput.value / 100) + "px";
hsvCanvasThumb.style.top = (pickerWidth * (100 - hsvValueInput.value) / 100) + "px";
drawPickerGradient(hsvHueInput.value);
updateColors();
// スライダーのイベントリスナー登録
hsvHueInput.addEventListener("input", function() {
drawPickerGradient(this.value);
updateColors();
});
// hsvCanvas, hsvCanvasThumb のイベントリスナー登録
hsvCanvas.addEventListener("pointermove", handlePickerPointerMove);
hsvCanvas.addEventListener("pointerdown", handlePickerPointerMove);
hsvCanvasThumb.addEventListener("pointerdown", function(event) {
// ポインターイベントのキャプチャターゲットを hsvCanvas に設定
hsvCanvas.setPointerCapture(event.pointerId);
});
/**
* カラーピッカーの canvas にグラデーションを描画
* @param {number} h 描画するグラデーションの色相(Hue)
*/
function drawPickerGradient(h) {
const canvas = document.getElementById("hsvCanvas");
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let s = (x / width) * 100;
let v = 100 - (y / height) * 100;
const rgb = hsvToRgb(h, s, v);
ctx.fillStyle = "rgb(" + rgb.r + ", " + rgb.g + ", " + rgb.b + ")";
ctx.fillRect(x, y, 1, 1);
}
}
}
/**
* カラーピッカーの色情報を更新
*/
function updateColors() {
// HSV から変換
const hex = hsvToHex(hsvHueInput.value, hsvSaturationInput.value, hsvValueInput.value);
// thumb のカラーを変更
root.style.setProperty("--thumb-color-slider--hue", hsvHueInput.value);
root.style.setProperty("--thumb-color-picker", "#" + hex);
// hexInput の値を更新
hexInput.value = "#" + hex;
}
/**
* キャンバス上でポインターを動かしたときの処理
* @param {object} PointerEvent
*/
function handlePickerPointerMove(event) {
// マウスの左ボタン押下中もしくはタップ中かどうか
if(event.buttons == 1) {
// ポインターイベントのキャプチャターゲットを hsvCanvas に設定
hsvCanvas.setPointerCapture(event.pointerId);
// ポインターの位置を取得(最小: 0, 最大: hsvCanvas のサイズ)
let top = Math.max(0, Math.min(event.offsetY, pickerHeight));
let left = Math.max(0, Math.min(event.offsetX, pickerWidth));
// hsvCanvasThumb の位置を変更
hsvCanvasThumb.style.top = top + "px";
hsvCanvasThumb.style.left = left + "px";
// ポインターの位置(%)を計算
const hsvSaturation = left / pickerWidth * 100;
const hsvValue = (1 - top / pickerHeight) * 100;
// HSV input の値を更新
hsvSaturationInput.value = hsvSaturation;
hsvValueInput.value = hsvValue;
updateColors();
}
}
});
/**
* HSV 形式を 16進数形式に変換
* @param {number} h 色相(Hue)
* @param {number} s 彩度(Saturation)
* @param {number} v 明度(Value)
*/
function hsvToHex(h, s, v) {
s /= 100;
v /= 100;
let c = v * s;
let x = c * (1 - Math.abs((h / 60) % 2 - 1));
let m = v - c;
let r, g, b;
if (h < 60) {
r = c, g = x, b = 0;
} else if (h < 120) {
r = x, g = c, b = 0;
} else if (h < 180) {
r = 0, g = c, b = x;
} else if (h < 240) {
r = 0, g = x, b = c;
} else if (h < 300) {
r = x, g = 0, b = c;
} else {
r = c, g = 0, b = x;
}
r = Math.round((r + m) * 255);
g = Math.round((g + m) * 255);
b = Math.round((b + m) * 255);
return (r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0')).toUpperCase();
}
/**
* HSV 形式を RGB 形式に変換
* @param {number} h 色相(Hue)
* @param {number} s 彩度(Saturation)
* @param {number} v 明度(Value)
*/
function hsvToRgb(h, s, v) {
s /= 100;
v /= 100;
let r, g, b;
const i = Math.floor(h / 60);
const f = (h / 60) - i;
const p = v * (1 - s);
const q = v * (1 - s * f);
const t = v * (1 - s * (1 - f));
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
動作サンプルのコード解説
ここからは、動作サンプルのコードの解説です。すべて解説すると非常に長くなってしまうので、重要な部分をピックアップして解説します。
HTML のコード解説
キャンバス部分
まずは、HTML のキャンバス部分についての解説です。グラデーションのキャンバスと、その中にあるツマミが含まれます。
<div class="hsv-canvas-wrapper">
<canvas id="hsvCanvas"></canvas>
<div id="hsvCanvasThumb" class="thumb"></div>
<input id="hsvSaturationInput" type="text" value="0">
<input id="hsvValueInput" type="text" value="100">
</div>
.hsv-canvas-wrapper
:
キャンバスのラッパー。#hsvCanvasThumb
の移動範囲を制限するためのもの。#hsvCanvas
:
グラデーションを描画するためのcanvas
。CSS では HSV の色空間が使用できないのでcanvas
でグラデーションを描画する。なぜ HSV を使う必要があるのかというと、その方が計算が楽なため。#hsvCanvasThumb
:
キャンバス上のツマミ。どこが選択されているかをわかりやすくするための目印。#hsvSaturationInput
:
彩度を保持しておくためのもの。#hsvCanvasThumb
の X 座標を%
で入れておく(左が 0% で、右が 100%)。#hsvValueInput
:
明度を保持しておくためのもの。#hsvCanvasThumb
の Y 座標を%
で入れておく(上が 100% で、下が 0%)。
スライダー部分
HTML のスライダー部分です。
<input id="hsvHueInput" type="range" name="hsv-hue" min="0" max="360" value="0">
#hsvHueInput
:
色相を選択するスライダー。色相環の角度をこのスライダーで設定する。最小値は0
、最大値は360
。
色相環についてはこちらのページがわかりやすいです。
<hue> - CSS: カスケーディングスタイルシート | MDN
CSS のコード解説
続いて、CSS の解説です。
CSS 変数の定義部分
CSS は、スライダーのクロスブラウザ対応やツマミの見た目のスタイリングに共通で使う値が多いので、変数を使って定義しています。
:root {
--track-width: 100%;
--track-height: 8px;
--track-background: linear-gradient(to right, hsl(0deg 100% 50%), hsl(60deg 100% 50%), hsl(120deg 100% 50%), hsl(180deg 100% 50%), hsl(240deg 100% 50%), hsl(300deg 100% 50%), hsl(360deg 100% 50%));
--track-border-width: 0;
--track-border-color: transparent;
--track-border-radius: 6px;
--thumb-width: 24px;
--thumb-height: 24px;
--thumb-color-picker: transparent;
--thumb-color-slider--hue: 0;
--thumb-color-slider: hsl(var(--thumb-color-slider--hue) 100 50);
--thumb-border-width: 2px;
--thumb-border-color: #FFFFFF;
--thumb-border-radius: 12px;
--thumb-shadow: -2px 2px 2px rgb(11 0 0 / 0.2);
}
特に重要なのは、ハイライト表示した箇所です。
--track-background
:
スライダーの背景のグラデーション。hsl()
関数を使用して、色相環をグルっと回った色を線形グラデーションとして描画(他のところでは HSV を使ってるのに、ここだけ HSL なのはご愛嬌ということで…。 Google のカラーピッカーでも同じようになっていたので、おそらくこれでも問題ないと思います)。--thumb-color-slider--hue
:
スライダーで選択した色相環の角度を CSS で使えるようにする変数。0
から360
の値を JavaScript でこの変数にセット。--thumb-color-slider
:
スライダーのツマミの色を指定する変数。hsl()
関数に--thumb-color-slider--hue
の値を指定することで、選択した値によってツマミの色が変わるようにする。
カラーピッカー全体
カラーピッカー全体に関わるスタイルの部分です。
.hsv-color-picker {
margin-inline: auto;
width: 240px;
}
.hsv-color-picker
:
カラーピッカー全体(スライダーも含む)を囲むクラス。width
でカラーピッカー全体の幅を設定。
キャンバス部分
グラデーションのキャンバスと、その中のツマミに関するスタイルです。
.hsv-canvas-wrapper {
position: relative;
margin-bottom: 8px;
touch-action: none;
}
.hsv-canvas-wrapper canvas {
display: block;
width: 100%;
aspect-ratio: 1 / 1;
}
.hsv-canvas-wrapper .thumb {
position: absolute;
top: 0;
left: 0;
width: var(--thumb-width);
height: var(--thumb-height);
border: solid var(--thumb-border-width) var(--thumb-border-color);
border-radius: var(--thumb-border-radius);
background: var(--thumb-color-picker);
box-shadow: var(--thumb-shadow);
box-sizing: border-box;
transform: translate(calc(var(--thumb-height) / 2 * -1), calc(var(--thumb-height) / 2 * -1));
}
.hsv-canvas-wrapper
:
キャンバスのラッパー。ツマミがabsolute
のため、ここにrelative
を指定。touch-action: none;
を指定することで、タッチデバイスでのドラッグ中に画面がスクロールしないようにする。.hsv-canvas-wrapper canvas
:
グラデーションを描画するcanvas
本体。aspect-ratio: 1 / 1;
を指定して、正方形になるようにする。正方形じゃなくてもいい場合は、ここの比率を変える。.hsv-canvas-wrapper .thumb
:
キャンバス上のツマミ。見た目がスライダーのツマミと同じになるようにスタイリング。
キャンバス上を動かすため、position: absolute;
を指定。
中心が選択位置になるように、translate
で位置を縦横半分ずつズラす。
スライダー部分
スライダー部分のスタイルです。
input[type="range"][name="hsv-hue"]::-webkit-slider-runnable-track {
height: var(--track-height);
border: solid var(--track-border-width) var(--track-border-color);
border-radius: var(--track-border-radius);
background-image: var(--track-background);
}
input[type="range"][name="hsv-hue"]::-webkit-slider-thumb {
margin-top: calc((var(--thumb-height) - var(--track-height)) / 2 * -1);
width: var(--thumb-width);
height: var(--thumb-height);
border: solid var(--thumb-border-width) var(--thumb-border-color);
border-radius: var(--thumb-border-radius);
background: var(--thumb-color-slider);
box-shadow: var(--thumb-shadow);
}
input[type="range"][name="hsv-hue"]::-moz-range-track {
height: var(--track-height);
border: solid var(--track-border-width) var(--track-border-color);
border-radius: var(--track-border-radius);
background-image: var(--track-background);
}
input[type="range"][name="hsv-hue"]::-moz-range-thumb {
width: var(--thumb-width);
height: var(--thumb-height);
border: solid var(--thumb-border-width) var(--thumb-border-color);
border-radius: var(--thumb-border-radius);
background: var(--thumb-color-slider);
box-shadow: var(--thumb-shadow);
}
WebKit 系と Mozilla 系に分かれているだけで、書いてあることはほとんど同じです。分けてある理由は、異なるベンダープレフィックス付きの疑似要素はまとめて記述ができないためです。
WebKit 系では、スライダーのスタイルをリセットするとツマミの位置が下にズレるので、それを修正するための行を追加しています。その辺りの挙動については、こちらの記事で詳しく解説しているので、よかったらご参照ください。
<input type="range">スライダーの見た目をCSSでカスタマイズする
JavaScript のコードの解説
そして、最後に JavaScript のコード解説です。
初期設定の部分
読み込み時の初期設定を行う部分です。
// hsvCanvas のサイズ取得
const pickerWidth = hsvCanvas.getBoundingClientRect().width;
const pickerHeight = hsvCanvas.getBoundingClientRect().height;
// 初期設定
hsvCanvasThumb.style.left = (pickerWidth * hsvSaturationInput.value / 100) + "px";
hsvCanvasThumb.style.top = (pickerWidth * (100 - hsvValueInput.value) / 100) + "px";
drawPickerGradient(hsvHueInput.value);
updateColors();
- hsvCanvas のサイズ取得 :
キャンバスのサイズを取得。 - 初期設定 :
ツマミの初期位置やグラデーションの描画、カラーピッカー内の色の情報などの初期設定を行う。これにより、リロード時にinput
の値が残っていた場合は、その値が反映される。
スライダーのイベントリスナー登録
スライダーを動かした時のイベントリスナー登録です。
// スライダーのイベントリスナー登録
hsvHueInput.addEventListener("input", function() {
drawPickerGradient(this.value);
updateColors();
});
グラデーションの再描画と、カラーピッカー内の色の情報の更新を行います。
ドラッグ時のイベントリスナー登録
キャンバス上をドラッグしたときのイベントリスナー登録です。
// hsvCanvas, hsvCanvasThumb のイベントリスナー登録
hsvCanvas.addEventListener("pointermove", handlePickerPointerMove);
hsvCanvas.addEventListener("pointerdown", handlePickerPointerMove);
hsvCanvasThumb.addEventListener("pointerdown", function(event) {
// ポインターイベントのキャプチャターゲットを hsvCanvas に設定
hsvCanvas.setPointerCapture(event.pointerId);
});
ドラッグイベントはキャンバスで拾うようになっています(ツマミはそれに合わせて動くだけ)。キャンバスをクリックした際にもその位置が選択されるようにするため、pointerdown
にもイベントリスナーを登録しています。また、ここではツマミのクリックからドラッグを開始できる処理も行っています。
hsvCanvas.setPointerCapture(event.pointerId);
:
ツマミ自体をクリック(もしくはタップ)した際にもドラッグが開始出来るように、ツマミのpointerdown
イベントが発生したときにポインターイベントのキャプチャターゲットをキャンバスに変更。
ドラッグ中の処理を行う関数
少しコード内の順番が前後しますが、ドラッグ中の処理を行う関数の解説です。
/**
* キャンバス上でポインターを動かしたときの処理
* @param {object} PointerEvent
*/
function handlePickerPointerMove(event) {
// マウスの左ボタン押下中もしくはタップ中かどうか
if(event.buttons == 1) {
// ポインターイベントのキャプチャターゲットを hsvCanvas に設定
hsvCanvas.setPointerCapture(event.pointerId);
// ポインターの位置を取得(最小: 0, 最大: hsvCanvas のサイズ)
let top = Math.max(0, Math.min(event.offsetY, pickerHeight));
let left = Math.max(0, Math.min(event.offsetX, pickerWidth));
// hsvCanvasThumb の位置を変更
hsvCanvasThumb.style.top = top + "px";
hsvCanvasThumb.style.left = left + "px";
// ポインターの位置(%)を計算
const hsvSaturation = left / pickerWidth * 100;
const hsvValue = (1 - top / pickerHeight) * 100;
// HSV input の値を更新
hsvSaturationInput.value = hsvSaturation;
hsvValueInput.value = hsvValue;
updateColors();
}
}
if(event.buttons == 1)
:
マウスの左ボタンを押下中もしくはタップ中のみ処理を行うようにする。hsvCanvas.setPointerCapture(event.pointerId);
:
ツマミの時と用途が違い、こちらはドラッグ中にキャンバスからポインターが外れた場合でもイベントを拾い続けるようにするもの。これにより、ドラッグ中にキャンバスの外に出てもドラッグ状態が継続する。- ポインターの位置を取得 :
ポインターの位置がキャンバス上のどこにあるかをpx
で取得。 - ポインターの位置(%)を計算 :
上で取得したpx
を元に、ポインターの位置を%
で計算。HSV 形式の色空間では、彩度と明度の指定にこの数値がそのまま使用できる(canvas
を使用したのはこのため)。
グラデーションを描画する関数
canvas
にグラデーションを描画する関数です。
/**
* カラーピッカーの canvas にグラデーションを描画
* @param {number} h 描画するグラデーションの色相(Hue)
*/
function drawPickerGradient(h) {
const canvas = document.getElementById("hsvCanvas");
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let s = (x / width) * 100;
let v = 100 - (y / height) * 100;
const rgb = hsvToRgb(h, s, v);
ctx.fillStyle = "rgb(" + rgb.r + ", " + rgb.g + ", " + rgb.b + ")";
ctx.fillRect(x, y, 1, 1);
}
}
}
スライダーで選択した色相を元にグラデーションを描画します。ここの処理や、途中の HSV から RGB に変換するhsvToRgb()
などは、ChatGPT にお願いしたら書いてくれました。
色情報を更新する関数
/**
* カラーピッカーの色情報を更新
*/
function updateColors() {
// HSV から変換
const hex = hsvToHex(hsvHueInput.value, hsvSaturationInput.value, hsvValueInput.value);
// thumb のカラーを変更
root.style.setProperty("--thumb-color-slider--hue", hsvHueInput.value);
root.style.setProperty("--thumb-color-picker", "#" + hex);
// hexInput の値を更新
hexInput.value = "#" + hex;
}
カラーピッカー内の色情報を更新する関数です。ここで、16 進数への変換や、ツマミの色の変更、最終的なカラーコードの出力を行っています。ここの途中に出てくるhsvToHex()
も ChatGPT が書いてくれました。
以上で、コードについての解説は終わりです。
終わりに
HTML, CSS, JavaScript を使って、オリジナルのカラーピッカーを作成する方法を紹介しました。ブラウザ標準のカラーピッカーではデザインを統一することが困難ですが、自作することで自由にカスタマイズすることができます。
さらに、カラーパレット機能を追加したり、16 進数以外に RGB や HSV, HSL の値を表示できるように拡張したりすることで、より高度なカラーピッカーを実装できます。ぜひ、今回紹介したコードを活用してみてください!