# ドラッグでしか色を選べず、値を入力できないカラーピッカー

Source: https://easeweb.dev/learn/colour-pickers
Topics: カスタム入力 > 色の選択で値を直接入力できる (WCAG 2.1.1, 4.1.2, 2.5.7)
The fix: ピッカーの横にラベル付きのカラーコードの入力欄を加えるか、ブラウザー標準のカラー入力を使うか、四角い面と色相のバーをrange入力で作り直して横にカラーコードの入力欄を置きます。作り直したrange入力は矢印キーで動き、値が読み上げられ、クリックした位置に色が設定されます。最初の2つは単独では不十分です。入力欄だけではドラッグできない人がコードでしか色を選べず、ブラウザーのカラー入力は見た目も動作もブラウザーごとに違います。
Test with: keyboard, screen reader, mouse, touch
References: https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F108 https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/color https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/range

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

ある予約アプリでは、店舗ごとにブランドカラーを設定でき、その色が予約ページの「今すぐ予約」ボタンに使われます。設定画面のピッカーは、デザインツールでおなじみの形です。四角い面は、横方向に白から鮮やかな色へ、縦方向に明るい色から黒へと変わります。その下に色相を選ぶ虹色のバーがあり、バーの下にはカラーコード「#366cd9」が書かれています。ピッカーの横のプレビューには、選んだ色で描いた予約ページのボタンが表示されます。マウスでは、四角い面の丸いつまみをつかんでドラッグし、次にバーのつまみをドラッグします。面のほかの場所をクリックしても何も起きず、動くのはつまみだけです。

Tabキーを押すと、フォーカスは設定画面の先頭から「保存」へ飛びます。ピッカーのどこにもフォーカスは移りません。スクリーンリーダーは、たどり着けば「#366cd9」を文字として読みますが、変更できるものは何もありません。ブランドガイドを開いて、そこに書かれたカラーコードを入力しようとしても、入力する場所がありません。

## 確認できる問題

つまみはスクリプトがポインターイベントで動かす `span` 要素で、スクリプトが押下を受け付けるのはつまみの上だけです。ブラウザーにとって、四角い面とバーはCSSのグラデーションで描いた絵で、つまみは中身のないspanです。そのため、Tabで止まる場所にも、キーで動く部品にもならず、読み上げる内容もありません。このピッカーは3つの達成基準を同時に満たしません。ピッカーのどの操作もキーボードではできないため、2.1.1を満たしません。つまみには役割も名前も値もなく、スクリーンリーダーはここに操作部品があることも、今どの色かも伝えられないため、4.1.2を満たしません。バーの下のコードは文字で、入力欄ではありません。色を変える方法がドラッグしかないため、2.5.7も満たしません。色を選ぶのにドラッグは本質的ではなく、2.5.7の解説書（Understanding）も、色の値を入力するテキスト欄や、カラーホイールの別の位置のクリックを、ドラッグせずに色を設定する方法として挙げています。

キーボードに対応しただけでは2.5.7は満たせず、入力欄を足しただけですべてが解決するわけでもありません。タッチスクリーンの利用者には矢印キーがないため、キーとドラッグでしか動かない面は、やはり使えません。また、ドラッグできないが色は目で選びたい人にとって、入力欄しかない状態は不十分です。そのため、推奨する修正では両方を用意します。

## 利用者への影響

ブランドカラーは一度設定すると、その店舗の予約サイトのすべてのページに表示されます。自分で設定できない人は、誰かに頼むしかありません。スマートフォンではつまみが小さく、選んでいる色を指が隠すため、誰にとっても思いどおりの色に合わせるのは難しくなります。

## 自動チェックとAIの限界

自動チェックに見えるのは、2つのグラデーションと、いくつかの中身のないspanと、1行の文字で、問題は見つかりません。どれも操作部品であることを示していないので、名前やTabの停止位置を求めるルールの対象になりません。監査のキーボードチェックは、role属性や、マークアップに直接書かれたクリック処理を手がかりに操作部品を見つけますが、このつまみにはどちらもありません。EaseWebの監査は、CSSのグラデーションで描かれた四角い色の面や色相のバーに、つまみはあるのに、フォーカスを受けるもの（スライダーや入力欄）が中に何もない場合、確認を促す項目を出します。ページのほかの場所で色を設定できるかは、人が試さないと分からないからです。コード生成AIにピッカーの修正を頼むと、四角い面のつまみに `role="slider"` と `tabindex="0"` を付けて終わることがあります。スライダーの値は1つですが、面の値は2つあり、コードを入力する欄も現れません。

## 修正前：ドラッグでしか動かない四角い面と色相のバー

```html
<div class="brand-colour">
  <span class="label">ブランドカラー</span>
  <div class="sv-area" style="background-color: hsl(220 100% 50%)">
    <span class="handle" style="left: 75%; top: 15%"></span>
  </div>
  <div class="hue-strip">
    <span class="handle" style="left: 61%"></span>
  </div>
  <div class="preview" aria-hidden="true" style="--brand: #366cd9">…</div>
  <span class="hex">#366cd9</span>
</div>
```

```js
for (const handle of picker.querySelectorAll('.handle')) {
  handle.addEventListener('pointerdown', startDrag);
}
// pointermoveでつまみを動かしてプレビューとコードの文字を更新し、pointerupでドラッグを終える
```

つまみはフォーカスを受けないため、キーボードでは届きません。役割も名前も値もないため、支援技術はピッカーがあることを知りません。操作の入口はつまみの押下だけなので、どの色にもドラッグが必要で、コードは誰も入力できない場所に書かれているだけです。

## 修正後：ピッカーの横にカラーコードの入力欄

```html
<div class="brand-colour" role="group" aria-labelledby="brand-label">
  <span class="label" id="brand-label">ブランドカラー</span>
  <div class="sv-area" style="background-color: hsl(220 100% 50%)" aria-hidden="true">
    <span class="handle" style="left: 75%; top: 15%"></span>
  </div>
  <div class="hue-strip" aria-hidden="true">
    <span class="handle" style="left: 61%"></span>
  </div>
  <div class="preview" aria-hidden="true" style="--brand: #366cd9">…</div>
  <label for="brand-hex">カラーコード</label>
  <input type="text" id="brand-hex" value="#366cd9" maxlength="7" spellcheck="false" autocomplete="off">
</div>
```

```js
for (const handle of picker.querySelectorAll('.handle')) {
  handle.addEventListener('pointerdown', startDrag);
}
const hex = document.getElementById('brand-hex');
// 入力されたコードで、ピッカー自身のsetColour()がつまみとプレビューを動かし…
hex.addEventListener('input', () => {
  const code = hex.value.trim();
  if (/^#?[0-9a-f]{6}$/i.test(code)) setColour(code.startsWith('#') ? code : `#${code}`);
});
// …ドラッグは入力欄に書き込む：hex.value = colour;
```

ピッカーはドラッグで選びたい人のためにそのまま残し、ラベル付きのテキスト入力欄を加えます。色はコードで、キー入力でも貼り付けでも音声でも入れられます。HTML標準の入力欄なので、Tabの順序に入り、役割、名前（「ブランドカラー」のグループ内の「カラーコード」）、値を持ちます。ドラッグした結果もこの欄に書き込まれるため、欄には常につまみで設定した色が表示されます。`aria-hidden` でドラッグ専用の面とバーをアクセシビリティツリーから外すので、スクリーンリーダーには役割も名前もない2つのつまみではなく、入力欄が見つかります。既存のピッカーへの変更は最小で、誰でも正確な値を指定できます。ただし、これだけでは不十分です。マウスの利用者はつまみを少し動かすだけで同じ青を少し明るくできますが、キーボードや音声操作の利用者は新しいコードを自分で考えなければなりません。ブランドガイドのように、使うコードが先に決まっている設定には向きますが、目で見て色を選ぶ設定には向きません。

## 修正後：ブラウザー標準のカラー入力

```html
<div class="brand-colour">
  <label for="brand-colour">ブランドカラー</label>
  <input type="color" id="brand-colour" value="#366cd9">
  <output for="brand-colour">#366cd9</output>
  <div class="preview" aria-hidden="true" style="--brand: #366cd9">…</div>
</div>
```

```js
const input = document.getElementById('brand-colour');
const [output, preview] = ['output', '.preview'].map((part) => input.parentElement.querySelector(part));
input.addEventListener('input', () => {
  output.textContent = input.value;
  preview.style.setProperty('--brand', input.value);
});
```

ページにはHTML標準の操作部品が入ります。Tabの順序に入り、ラベルで名前が付き、EnterかSpaceでピッカーが開き、値は小文字6桁の16進数のコードです（新しい `alpha` 属性や `colorspace="display-p3"` を指定すると、対応するブラウザーでは `color()` の値になります）。ピッカーはブラウザーのものなので、2.5.7でもブラウザーの責任になります。ただし、開くピッカーはブラウザーごとに違い、ページからはどれも変えられません。

- パソコンのChromeとEdgeは、独自のポップアップを開く。矢印キーで動く色の面と色相のスライダー、値の入力欄（最初は赤・緑・青で、ボタンでHSLと16進数に切り替わる）、スポイトがある。
- MacのSafariは、色の並んだ小さなパネルを開き、そこからMacの「カラー」ウインドウを開ける。そのスライダーにはカラーコードの入力欄がある。iPhoneとiPadのSafariはシステムのカラーピッカーを開き、そのスライダーのタブにもカラーコードの入力欄がある。
- パソコンのFirefoxは、OSのダイアログを開く。Windowsのダイアログは、赤・緑・青、または色合い・鮮やかさ・明るさを数値で受け付けるが、16進数のコードは入力できない。
- AndroidのChromeは、いくつかの色と、3本のスライダーの「カスタム」画面を表示し、入力欄はない。

つまり、多くのブラウザーでは値を入力できますが、すべてではありません。閉じた状態の操作部品は小さな色見本で、ページから変えられる見た目にも限りがあります。必要なコードは最も少なく、あまり使われない設定なら選択肢になりますが、利用者が使うすべてのブラウザーで確認してください。

## 修正後：四角い面とバーをスライダーにし、カラーコードの入力欄を添える

```html
<div class="brand-colour" role="group" aria-labelledby="brand-label">
  <span class="label" id="brand-label">ブランドカラー</span>
  <div class="sv-area" style="background-color: hsl(220 100% 50%)">
    <span class="handle" style="left: 75%; top: 15%"></span>
    <input type="range" class="visually-hidden" id="saturation" aria-label="彩度" min="0" max="100" value="75" aria-valuetext="75%">
    <input type="range" class="visually-hidden" id="brightness" aria-label="明るさ" min="0" max="100" value="85" aria-valuetext="85%">
  </div>
  <input type="range" class="hue-strip" id="hue" aria-label="色相" min="0" max="359" value="220" aria-valuetext="220°、青">
  <div class="preview" aria-hidden="true" style="--brand: #366cd9">…</div>
  <label for="brand-hex">カラーコード</label>
  <input type="text" id="brand-hex" value="#366cd9" maxlength="7" spellcheck="false" autocomplete="off">
</div>
```

```js
const [sat, bright, hue, hex] = ['saturation', 'brightness', 'hue', 'brand-hex'].map((id) => document.getElementById(id));
const [area, handle, preview] = ['.sv-area', '.sv-area .handle', '.preview'].map((part) => picker.querySelector(part));
const HUES = [[15, '赤'], [45, 'オレンジ'], [70, '黄'], [165, '緑'], [195, '水色'], [255, '青'], [290, '紫'], [345, 'ピンク'], [360, '赤']];
// つまみ、面の色、読み上げる値をスライダーに合わせる
function place() {
  sat.setAttribute('aria-valuetext', `${sat.value}%`);
  bright.setAttribute('aria-valuetext', `${bright.value}%`);
  hue.setAttribute('aria-valuetext', `${hue.value}°、${HUES.find(([end]) => hue.valueAsNumber < end)[1]}`);
  area.style.backgroundColor = `hsl(${hue.value} 100% 50%)`;
  handle.style.left = `${sat.value}%`;
  handle.style.top = `${100 - bright.value}%`;
}
function show(colour) {
  preview.style.setProperty('--brand', colour);
}
// スライダーが色を決める。toHex()とtoHsv()はピッカー自身の変換
for (const range of [sat, bright, hue]) {
  range.addEventListener('input', () => {
    place();
    hex.value = toHex(hue.valueAsNumber, sat.valueAsNumber, bright.valueAsNumber);
    show(hex.value);
  });
}
// 面のどこを押しても、その位置に両方の値を設定する。ドラッグは不要
area.addEventListener('pointerdown', (event) => {
  const box = area.getBoundingClientRect();
  sat.value = Math.round(((event.clientX - box.left) / box.width) * 100);
  bright.value = Math.round(100 - ((event.clientY - box.top) / box.height) * 100);
  sat.dispatchEvent(new Event('input'));
  sat.focus();
  startDrag(event); // 押したまま動かせば、これまでどおりドラッグになる
});
// 入力されたコードでスライダーを動かし、色は入力したとおりに保つ
hex.addEventListener('input', () => {
  const code = hex.value.trim();
  if (!/^#?[0-9a-f]{6}$/i.test(code)) return;
  [hue.value, sat.value, bright.value] = toHsv(code);
  place();
  show(code.startsWith('#') ? code : `#${code}`);
});
```

四角い面の見た目はそのままで、中に見えない2つのrange入力を持たせます。つまみの横の位置が示す「彩度」と、縦の位置が示す「明るさ」です。色相のバーはそれ自体をrange入力にし、虹色をトラックとして描きます。3つともHTML標準の入力なので、フォーカスを受け、矢印キー、Page Up、Page Down、Home、Endで動き、スクリーンリーダーが読む役割と値もブラウザーが用意します。色相の度数はほとんどの人には意味が伝わらないため、「220°、青」のように色の名前付きで読み上げます。面のどこを押してもその位置に両方の値が設定され、バーをクリックするとつまみがそこへ移るため、どの色にもドラッグは不要で、ドラッグしたい人はこれまでどおりドラッグできます。最初の方法のカラーコードの入力欄も残すので、ブランドガイドのコードを貼り付けられ、その値はそのまま保たれます。スライダーの整数に変換して戻すと、値が丸められてしまうからです。見えない入力にはフォーカスの枠が表示されないため、どちらかにフォーカスがある間は、面のつまみにフォーカスの枠を描きます。

## どの方法を選ぶか

3つとも適合します。カラーコードの入力欄は変更が最小で、誰でも正確な値を指定できますが、ドラッグできない人はコードでしか色を選べません。ブラウザー標準のカラー入力は必要なコードが最も少ないものの、開くピッカーがブラウザーごとに違い、すべてで値を入力できるわけではなく、ページの中に置くことも、ポップアップの見た目を変えることもできません。キーボードやスクリーンリーダーの利用者も目で見て、またはコードで色を選べ、どのブラウザーでも同じように動き、デザインどおりのピッカーも保てるのは、スライダーとカラーコードの入力欄を組み合わせる方法だけです。ピッカーを自分で作るなら、この方法を使ってください。

## 実装の判断

スクリーンリーダーが読み上げる内容は、聞く人に意味が伝わるものにします。16進数のコードはほとんどの人には意味が分からず、彩度の「75%」も大差ありません。色相の `aria-valuetext` には色の名前を入れ、コードの横に色全体の名前（「濃い青」など）を書いて、誰でも読めるようにすることも検討します。プレビューは予約ページの絵で、操作部品ではないため、支援技術からは隠し、フォームのボタンとは離して描きます。Chromeは、標準のカラー入力の値を、赤・緑・青の割合として英語のままスクリーンリーダーに渡します（「21% red 42% green 85% blue」）。

ページに組み込んだ後の動作は、次の点で決まります。

- Tabの停止位置を1つにするか2つにするか：面の中の2つの入力はTabの停止位置が2つになるが、作りが単純で、ブラウザー標準のキー操作が使える。ライブラリーによっては面の停止位置を1つにし、左右の矢印キーで一方の見えない入力を、上下の矢印キーでもう一方を動かす。React Ariaのカラーエリアはこの作りである。
- フォーカスとコントラスト：見えない入力はフォーカスの枠を描かないため、`.sv-area:has(:focus-visible) .handle` のようにしてつまみに枠を描く。白い輪に濃い縁を付けると、面のどの色の上でもつまみが見える。[コントラストが足りない入力欄・アイコン・フォーカス枠](/ja/learn/non-text-contrast)を参照。
- カラーコードの入力欄：`#` があってもなくても受け付ける。正しくないコードは黙って元に戻さず、入力されたまま残して、欄の横にメッセージを出す。[直し方がわからないフォームエラー](/ja/learn/accessible-form-errors)を参照。
- タッチ：面とバーに `touch-action: none` を付けるとドラッグでページがスクロールしなくなるが、そこに指を置いてページをスクロールすることもできなくなる。周りに常に余白が残る大きさにとどめる。
- 入力欄から開くピッカー：カラーコードの入力欄にフォーカスが移ったときに、その下に面とバーを開いてもかまわない。ただし、フォーカスは入力欄に残り、入力を続けられ、Escapeで閉じ、次の入力欄を覆わないようにする。フォーカスを動かさずに現れる内容はコンテンツの変化で、コンテキストの変化ではないが、フォーカスが離れるか利用者が閉じるまでは開いたままにし、フォーカスを動かさずに閉じられる必要がある。[操作するだけでページが変わるドロップダウンと入力欄](/ja/learn/context-changes)と[近づくと消え、閉じられないツールチップ](/ja/learn/hover-content)を参照。
- 強制配色：コントラスト テーマは、面の色相とプレビューを塗る背景色を置き換え、面の濃淡とバーを描くグラデーションも消す。面、バー、プレビューには `forced-color-adjust: none` を指定し、つまみにはシステムカラーの枠線を付ける。

カラーピッカーのライブラリーはさまざまです。面に `role="slider"` を付けながら `aria-valuenow` がないもの（axeが報告します）、キーは効いても面の押下を受け付けないもの、入力欄がまったくないものもあります。ライブラリーの説明ではなく、フォーカスを受ける各部分が支援技術に何を伝えているかを確認してください。

## 修正の確認

1. マウスを使わずにTabキーを押し、色を設定するすべての部分に到達できるか確認する。カラーコードの入力欄、各スライダー、またはブラウザーのカラー入力である。
2. #2a9d8f のような16進数のコードを、入力欄か、ブラウザーのピッカーにある入力欄に入力し、プレビューがその色そのものになり、ページがそれを保存するか確認する。
3. スクリーンリーダーで、各部分の名前、役割、値が読み上げられ、色相が色の名前付きで、または色がコードで読み上げられ、変更のたびに値が読み上げられるか確認する。
4. マウスでクリックだけを使い、新しい色を設定する。面のクリック、バーのクリック、コードの入力、ブラウザー標準のピッカーのどれでもよい。押したままポインターを動かす手順がないことを確認する。タッチスクリーンのタップと、音声操作でも同じように確認する。
5. ブラウザーのカラー入力を使う場合は、利用者が使う各ブラウザーで、パソコンとスマートフォンの両方で開き、キーボードで色を設定できるか、利用者に必要ならコードを入力できるかを確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。ピッカーやそのライブラリーを変更した際は、再度確認します。

## 制限

このサンプルは、どのアプリケーションでも同じ結果になることを保証するものではありません。range入力の読み上げ方はスクリーンリーダーによって異なり、名前より先に値を読むものや、iOSのようにキーではなく上下のスワイプで動かすものもあります。ブラウザー標準のカラーピッカーはバージョンによって変わるため、利用者が使うものを確認してください。価格や音量のような1本のスライダーは[ドラッグでしか動かない範囲スライダー](/ja/learn/sliders)、ジェスチャー全般は[ボタンで代われないピンチ・ドラッグ・傾きのジェスチャー](/ja/learn/gestures)で扱います。選んだ色の上で白い文字が読めるかどうかは別の確認です。[ブランドカラーや写真の上でコントラストが足りない文字](/ja/learn/text-contrast)を参照してください。対象とする支援技術で、設定の操作全体を確認してください。

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
