# キーボードでは表示できないグラフのツールチップ

Source: https://easeweb.dev/learn/chart-tooltips
Topics: 表とデータ > グラフの点、凡例、ツールチップをキーボードで操作できる (WCAG 2.1.1, 4.1.2)
The fix: chartjs-plugin-datalabels で各棒の件数をグラフに書き、ツールチップはポインター用に残します。または、キャンバスに role="application" を付けてTabキーで一度だけ止まるようにし、矢印キーで Chart.js のツールチップを棒から棒へ動かしながら、件数をステータスの行にも書き込みます。
Test with: keyboard, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F54 https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html https://www.w3.org/TR/wai-aria-1.2/#application https://www.chartjs.org/docs/latest/configuration/tooltip.html https://www.chartjs.org/docs/latest/configuration/interactions.html https://www.chartjs.org/docs/latest/samples/advanced/programmatic-events.html https://chartjs-plugin-datalabels.netlify.app/guide/positioning.html https://github.com/recharts/recharts/releases/tag/v3.0.0

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

## 場面

ネットショップの管理画面に、先週の日別の注文数を表す棒グラフがあります。棒には数字が書かれていません。土曜日の件数を知るには、土曜日の棒にポインターを重ねます。すると小さな黒いツールチップが開き、「土」と「注文数: 31」が出ます。棒から外れると閉じます。Chart.js では、ショップ側でコードを書かなくても、初めからこう動きます。

マウスを使わずにTabキーを押すと、フォーカスはカードより前にある要素から、グラフの下の「すべての注文を見る」へ一気に移ります。グラフにはフォーカスを受け取るものがなく、ツールチップを開くキーもないため、件数は一度も表示されません。キーボード利用者には、土曜日が一番多かったことは見えても、31件だったことや、火曜日が15件か14件かは分かりません。頼りになるのは、5件ごとに引かれた目盛りの線だけです。

グラフにはテキストの代替もあります。[スクリーンリーダーで数値が伝わらないグラフ](/ja/learn/chart-alternatives)の一つ目の修正のとおり、名前にすべての曜日の件数が書かれているので、スクリーンリーダー利用者には件数が聞こえます。ただ、その文字は画面には出ないため、目で見ながらキーボードを使う人には届きません。

## 確認できる問題

WCAG 2.1.1「キーボード」は、ポインターでできることはすべてキーボードでもできるよう求めています。棒の件数を表示するのはこのグラフの機能の一つですが、それができるのはポインターだけです。Chart.js はツールチップをキャンバスに描き、`events` オプションに並ぶブラウザーのイベントで開きます。初期値は `mousemove`、`mouseout`、`click`、`touchstart`、`touchmove` で、どれもポインターか指によるものです。初期設定では、ツールチップが開くのはポインターが重なっている棒だけです。キャンバスは初期状態ではフォーカスを受け取らず、グラフはキー入力も受け付けていないので、キーボードでできることが何もありません。これは失敗例F54にあたります。値を表示するという機能を、ポインター専用のイベントハンドラーだけに結び付けているからです。

問題になるのは、件数を表示する場所がツールチップしかないことです。棒の上や、グラフの横の表にすでに書かれている数字をツールチップが繰り返しているだけなら、キーボード利用者が見られないものは何もありません。

## 利用者への影響

管理画面を開くのは、正確な数字を知るためです。土曜日の31件と金曜日の24件の差は、週末がまずまずだったか、忙しかったかの違いになります。Chart.js はタッチにも反応するため、タッチ画面では問題なく使えます。件数を一つも見られないのは、キーボードで操作する人と、キーボードの操作に置き換えて動く道具を使う人です。

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

ツールチップは、ポインターを重ねている間だけキャンバスに描かれる絵なので、ページの中にチェックツールが調べられるものはありません。axe には `role="img"` と名前の付いたキャンバスが見えるだけで、ホバーもしないため、合格にします。この診断は、ライブラリーが描いたグラフの中に、フォーカスを受け取るものが一つもなければ、確認候補として出します。棒にポインターを重ねてから、同じことをキーボードで試すのは人の役割です。ツールチップの中身が、ほかの場所に書かれていない値かどうかは、ページからは判断できません。ツールチップ付きの棒グラフをAIのアシスタントに頼むと、ライブラリーの初期設定をそのまま使い、この修正前のコードと同じものになることがあります。

## 修正前：件数は Chart.js のツールチップの中だけ

```html
<section class="card">
  <h2>Orders per day</h2>
  <p>Last week</p>
  <canvas id="orders" role="img" aria-label="…"></canvas>
  <a href="/orders">View all orders</a>
</section>
```

```css
.card canvas {
  max-height: 200px;
}
```

```js
import Chart from 'chart.js/auto';

const canvas = document.getElementById('orders');
const chart = new Chart(canvas, {
  type: 'bar',
  data: {
    labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    datasets: [
      { label: 'Orders', data: [12, 15, 14, 18, 24, 31, 20], backgroundColor: 'rgba(74, 134, 212, 0.55)', borderColor: '#4a86d4', borderWidth: 1 }
    ]
  },
  options: { plugins: { legend: { display: false } } }
});
```

- 件数はツールチップの中にしか描かれず、ツールチップは棒にポインターを重ねている間しか開かない。
- キャンバスがフォーカスを受け取らないため、Tabキーはグラフを通り過ぎ、ツールチップを開くキーもない。

## 修正後：棒の上に件数を書く

```html
<section class="card">
  <h2>Orders per day</h2>
  <p>Last week</p>
  <canvas id="orders" role="img" aria-label="…"></canvas>
  <a href="/orders">View all orders</a>
</section>
```

```css
.card canvas {
  max-height: 200px;
}
```

```js
import Chart from 'chart.js/auto';
import ChartDataLabels from 'chartjs-plugin-datalabels';

const canvas = document.getElementById('orders');
const chart = new Chart(canvas, {
  type: 'bar',
  data: {
    labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    datasets: [
      { label: 'Orders', data: [12, 15, 14, 18, 24, 31, 20], backgroundColor: 'rgba(74, 134, 212, 0.55)', borderColor: '#4a86d4', borderWidth: 1 }
    ]
  },
  plugins: [ChartDataLabels],
  options: {
    layout: { padding: { top: 20 } },
    plugins: {
      legend: { display: false },
      datalabels: { anchor: 'end', align: 'end', color: '#1f2937' }
    }
  }
});
```

1. このグラフの `plugins` に chartjs-plugin-datalabels を加え、各棒の値を描かせます。
2. `anchor: 'end'` と `align: 'end'` を指定し、件数を、視線が自然に向かう棒の先端のすぐ上に置きます。
3. 件数の色を、白いカードに対して14.68:1の `#1f2937` にします。グラフの上に `layout.padding` で20ピクセルの余白を取り、一番高い土曜日の31が上で切れないようにします。

これで修正前の問題が解消し、2.1.1を満たします。ツールチップの中にしかない件数がなくなるので、キーボードで開く必要のあるものが残りません。ツールチップはそのまま残り、棒にポインターを重ねれば開きます。

## 修正後：矢印キーでツールチップを動かす

```html
<section class="card">
  <h2>Orders per day</h2>
  <p>Last week</p>
  <canvas id="orders" role="application" tabindex="0" aria-label="…" aria-describedby="orders-keys"></canvas>
  <p id="orders-keys" class="keys">Use the left and right arrow keys to read each day.</p>
  <p id="orders-day" class="visually-hidden" role="status"></p>
  <a href="/orders">View all orders</a>
</section>
```

```css
.card canvas {
  max-height: 200px;
}
.keys {
  margin: 4px 0 0;
  font-size: 13px;
  color: #4b5563;
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
```

```js
import Chart from 'chart.js/auto';

const canvas = document.getElementById('orders');
const chart = new Chart(canvas, {
  type: 'bar',
  data: {
    labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    datasets: [
      { label: 'Orders', data: [12, 15, 14, 18, 24, 31, 20], backgroundColor: 'rgba(74, 134, 212, 0.55)', borderColor: '#4a86d4', borderWidth: 1 }
    ]
  },
  options: { plugins: { legend: { display: false } } }
});

const status = document.getElementById('orders-day');
let day = -1;

function show(index) {
  day = index;
  const active = [{ datasetIndex: 0, index }];
  const bar = chart.getDatasetMeta(0).data[index];
  chart.setActiveElements(active);
  chart.tooltip.setActiveElements(active, { x: bar.x, y: bar.y });
  chart.update();
  status.textContent = `${chart.data.labels[index]}: ${chart.data.datasets[0].data[index]} orders`;
}

function hide() {
  chart.setActiveElements([]);
  chart.tooltip.setActiveElements([], { x: 0, y: 0 });
  chart.update();
  status.textContent = '';
}

canvas.addEventListener('focus', () => show(Math.max(day, 0)));
canvas.addEventListener('blur', hide);
canvas.addEventListener('keydown', (event) => {
  const last = chart.data.labels.length - 1;
  if (event.key === 'ArrowRight') show(Math.min(day + 1, last));
  else if (event.key === 'ArrowLeft') show(Math.max(day - 1, 0));
  else if (event.key === 'Home') show(0);
  else if (event.key === 'End') show(last);
  else if (event.key === 'Escape') hide();
  else return;
  event.preventDefault();
});
```

1. キャンバスに `tabindex="0"` を付けてTabキーの移動順に入れ、見出しとリンクの間で一度止まるようにします。
2. 名前はそのままに `role="application"` を付けます。グラフにフォーカスがある間、スクリーンリーダーは矢印キーを読み進めるのには使わず、ページに渡すようになります。
3. フォーカスを受け取ったら、最後に矢印キーで止まっていた曜日、初めてなら月曜日のツールチップを開きます。開くには、Chart.js 自身の `setActiveElements` をグラフと `chart.tooltip` の両方で呼びます。公式サンプル「Programmatic Event Triggers」でも使われている呼び出しです。
4. 左右の矢印キーで前後の曜日へ、HomeキーとEndキーで最初と最後の曜日へ動かします。Escapeキーを押したときと、フォーカスが離れたときにはツールチップを隠します。これで、キーで開いたツールチップを、フォーカスを動かさずに閉じられます。
5. 使うキーをグラフの下の1行に書き、キャンバスと `aria-describedby` で関連付けます。目で見ながらキーボードを使う人にも、グラフを順にたどれることが伝わります。
6. 各曜日のツールチップの内容を、画面には表示せず最初からページに置いた `role="status"` の行にも書き込みます。キーを押すたびに、スクリーンリーダーが「Sat: 31 orders」のように読み上げます。

これで修正前の問題が解消し、2.1.1を満たします。ポインターで出せる件数は、すべて矢印キーでも出せます。グラフはすっきりした見た目のままで、ホバーでもツールチップが開きます。

## どの方法を選ぶか

どちらの方法でも基準を満たします。件数を書く方法は、プラグイン一つとオプション四つで済み、キーボード利用者、画面を拡大している人、ホバーしようと思いつかない人のすべてに一度に役立ちます。この例のように棒が数本のグラフには向いていますが、棒が何十本もあったり系列がいくつも並んだりすると、数字が重なり合って読めなくなります。矢印キーの方法は、描いたとおりの見た目を保ち、点がいくつあっても使えますが、スクリプトと操作の説明の1行が必要で、ロールも正しく付けなければなりません。点の多いグラフや、数字を書くと形が埋もれてしまうグラフで選んでください。[スクリーンリーダーで数値が伝わらないグラフ](/ja/learn/chart-alternatives)のように、件数の表をグラフの横にすでに置いているなら、すべての値はキーボードで届く文字になっていて、ツールチップだけが頼りという状態ではなくなっています。

## 実装の判断

**ここでは `role="application"` が正しい理由と、ほかではまず正しくない理由。** `application` は、キーをページに渡すようスクリーンリーダーに伝え、読み上げ用のキーを止めます。読む内容がある部分に付けるのは誤りで、このロールがほとんどの場合に正解にならないのもそのためです。キャンバスの中には読むものがありません。必要なキーはすべてグラフ側で処理し、表示する件数もすべてステータスの行に書き込みます。Recharts 3 も初期設定で同じ作りです。グラフに `role="application"` と `tabindex="0"` を付け、矢印キーでツールチップを動かし、そのツールチップをライブリージョンにしています。Recharts 2 では、`accessibilityLayer` を指定しない限り、この機能はオフでした。

**フォーカスで開くツールチップは、フォーカスで表示されるコンテンツ。** グラフがフォーカスを受け取って開くツールチップは、WCAG 1.4.13がホバーやフォーカスで表示されるコンテンツに求めるとおり、フォーカスを動かさずにEscapeキーで閉じられ、閉じられるかフォーカスが離れるまでは開いたままでなければなりません。近づくと消えるツールチップや、下にあるものを覆ってしまうツールチップについては、[近づくと消え、閉じられないツールチップ](/ja/learn/hover-content)で説明しています。

**ステータスの行は短く。** キーを押すたびに読まれるので、曜日と件数だけにし、グラフの名前を繰り返さないようにします。

**ほかのライブラリー。** Highcharts のアクセシビリティモジュールは、点を順にたどるキーボード操作を加えます。D3 Graph Gallery のツールチップの例のように、`mouseover` でツールチップの `div` を表示し、`mouseout` で隠す手作りのSVGのグラフも、同じ問題を抱えています。修正方法も同じ二つです。各図形の上に `text` 要素を置くか、グラフ全体をTabキーで一度止まる場所にして、矢印キーで図形の間を移れるようにします。

クリックするとページを絞り込んだり詳細へ進んだりする棒は、同じ問題の別の形ですが、まだガイドはありません。クリックでしか系列を隠せない凡例は、[キーボードでは系列を切り替えられないグラフの凡例](/ja/learn/chart-legends)で扱っています。

## 修正の確認

1. コードで各件数がどこに表示されるかを探し、ツールチップに出る値がすべてグラフに書かれていること、またはキャンバスに `tabindex="0"`、`role="application"`、ツールチップを動かすキーの処理があることを確認する。
2. マウスを使わずに、Tabキーでカードの中を移動する。件数を書いた場合は、すべて読めることを確認する。矢印キーの場合は、Tabキーでグラフに止まってツールチップが開き、左右の矢印キーですべての曜日へ、HomeキーとEndキーで最初と最後へ移り、Escapeキーで閉じ、Tabキーで下のリンクへ進めることを確認する。
3. スクリーンリーダーを使ってTabキーでグラフへ移動し、名前が読み上げられることを確認する。矢印キーの修正では、キーを押すたびに「Sat: 31 orders」のように曜日と件数が読まれることを確認する。
4. ウィンドウの幅を320 CSSピクセルまで狭め、書いた件数が重ならずに棒の上に収まること、またはツールチップがグラフの中に収まることを確認する。

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

## 制限

このサンプルは、どのグラフでも同じ結果になることを保証するものではありません。`Chart` は Chart.js のことで、`setActiveElements` はコードからツールチップを開くための Chart.js の機能です。chartjs-plugin-datalabels は、Chart.js のプロジェクトが公開している別のパッケージです。ほかのライブラリーでは、それぞれにあたる機能に置き換えてください。`role="application"` の扱いはスクリーンリーダーによって異なり、矢印キーを読み上げに使い続けるものもあるかもしれません。管理画面全体を、キーボードと対象とする支援技術で確認してください。

## スクリーンリーダーでの動き（EaseWebが実機で確認）

`role="application" tabindex="0"`: 矢印キーで順に読むグラフのように、独自のキー操作を持つ部分です。ARIAは、そこではキーを読み上げに使わず、ページに渡すようスクリーンリーダーに求めています。

| スクリーンリーダー | ブラウザーとOS | 矢印キー |
| --- | --- | --- |
| NVDA | Chrome, Windows 11 | 未確認 |
| JAWS | Chrome, Windows 11 | 未確認 |
| VoiceOver | Safari, iOS | 未確認 |
| TalkBack | Chrome, Android | 未確認 |

`role="status"`: polite のライブリージョンです。変化は、スクリーンリーダーが読み終えるまで待ちます。

| スクリーンリーダー | ブラウザーとOS | 下矢印キー | 連続読み上げ |
| --- | --- | --- | --- |
| NVDA | Chrome, Windows 11 | 未確認 | 未確認 |
| JAWS | Chrome, Windows 11 | 未確認 | 未確認 |
| VoiceOver | Safari, iOS | 未確認 | 未確認 |
| TalkBack | Chrome, Android | 未確認 | 未確認 |

`aria-describedby`: ページ内の別のテキスト（ヒントやエラーなど）を、要素の説明にします。

| スクリーンリーダー | ブラウザーとOS | Tabで移動 | 下矢印キー |
| --- | --- | --- | --- |
| NVDA | Chrome, Windows 11 | 未確認 | 未確認 |
| JAWS | Chrome, Windows 11 | 未確認 | 未確認 |
| VoiceOver | Safari, iOS | 未確認 | 未確認 |
| TalkBack | Chrome, Android | 未確認 | 未確認 |

https://easeweb.dev/ja/learn/screen-reader-support

---

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