# キーボードで開けず、閉じるボタンにも名前がないチャット

Source: https://easeweb.dev/learn/chat-launchers
Topics: ダイアログとオーバーレイ > チャットの起動ボタンと閉じるボタンがキーボードで使え、役割が伝わる (WCAG 2.1.1, 4.1.2)
The fix: 起動ボタンと閉じるボタンを、言葉の名前を持つHTML標準のボタンにします。丸いアイコンのまま起動ボタンにaria-labelを付けるか、起動ボタンのアイコンの横に「チャットで相談」と表示します。どちらの場合も、×には「チャットを閉じる」のような名前を付けます。
Test with: keyboard, screen reader, speech input
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/ARIA/apg/patterns/button/ https://www.w3.org/WAI/WCAG22/Techniques/failures/F59 https://www.w3.org/WAI/WCAG22/Techniques/failures/F68

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

## 場面

家具の店Birchwood Homeのサイトでは、どのページの右下にも丸いチャットの吹き出しがあります。マウスの利用者がクリックすると、その場所にチャットのパネルが開きます。「Birchwoodのチャット」という見出し、閉じるための×、あいさつ、メッセージ欄です。キーボードの利用者がTabキーでページを下へ進むと、フォーカスは「配送料金」「注文の追跡」「返品」と移り、次のTabでページを離れてブラウザー自身のコントロールへ移ります。吹き出しには一度も届かず、フォーカスが届かないものはEnterキーでも開けません。別の方法でチャットを開いたスクリーンリーダーの利用者がTabキーで×へ移ると、「ボタン」と読まれるだけで、何をするボタンなのか、チャットを閉じるボタンなのかは分かりません。

## 確認できる問題

吹き出しは、クリック処理を付けた `div` の中にアイコンを入れたものです。`div` はフォーカスを受け取らないため、Tabキーは素通りし、EnterキーもSpaceキーも効きません。達成基準2.1.1は、マウスでできることはすべてキーボードでもできることを求めていますが、ここでチャットを開けるのはマウスだけです。

達成基準4.1.2は、すべてのコントロールに、支援技術が読み取れる名前と役割があることを求めています。吹き出しにはどちらもありません。ボタンではなく、言葉もなく、絵があるだけです。×は本物の `button` なので役割はあり、Tabキーでも届きますが、中には文字のないアイコンしかないため名前が空で、スクリーンリーダーは「ボタン」とだけ読みます。横に「送信」ボタンもあるパネルでは、押してみないとどちらがどちらか分かりません。W3Cは、どちらも4.1.2の失敗例として挙げています。スクリプトでコントロールにした `div` に役割がないもの（F59）と、名前のないコントロール（F68）です。

## 利用者への影響

チャットはどのページでも同じ隅にあり、困った人を案内する先になっていることも多いため、開けない人は、本来その人たちのために用意された助けを失います。

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

自動チェックから見ると、吹き出しはアイコンの入った `div` にすぎません。チャットを開く処理はスクリプトで追加されるため、マークアップにはその `div` がコントロールであることを示すものがなく、手がかりは、`cursor: pointer` の指の形のカーソルと、隅に固定された位置だけです。×の方は見つかりやすく、名前のないボタンはよくあるチェック項目です。ただし見つかるのはパネルが開いている間だけで、チャットのパネルの多くは、ページを開いた時点では閉じていて、開く操作がないかぎり閉じたままです。チャットウィジェットは提供元のスクリプトであることが多く、提供元のドメインのフレーム内に描かれることもあるため、サイト側のチームがそのコードを読むことはほとんどありません。チャットの吹き出しの実装をAIアシスタントに頼むと、クリックできる `div` を書くことがあります。マウスでは見た目も動作も同じだからです。

## 修正前：divの起動ボタンと名前のない×

```html
<div class="chat-launcher">
  <svg class="icon" viewBox="0 0 24 24">
    <use href="#icon-chat"></use>
  </svg>
</div>
<section id="chat" class="chat-panel" aria-labelledby="chat-title" hidden>
  <div class="chat-header">
    <h2 id="chat-title">Birchwoodのチャット</h2>
    <button type="button" class="chat-close">
      <svg class="icon" viewBox="0 0 24 24">
        <use href="#icon-close"></use>
      </svg>
    </button>
  </div>
  <div class="chat-messages" role="log" aria-label="メッセージ">
    <p class="message from-us">こんにちは。ご注文についてお気軽にお尋ねください。</p>
  </div>
  <form class="chat-form">
    <label for="chat-input" class="visually-hidden">メッセージ</label>
    <input id="chat-input" autocomplete="off">
    <button>送信</button>
  </form>
</section>
```

```js
const launcher = document.querySelector('.chat-launcher');
const panel = document.getElementById('chat');
const input = document.getElementById('chat-input');

// パネルは起動ボタンの場所に開く。
function openChat() {
  launcher.hidden = true;
  panel.hidden = false;
  input.focus();
}
function closeChat() {
  panel.hidden = true;
  launcher.hidden = false;
  launcher.focus();
}

launcher.addEventListener('click', openChat);
panel.querySelector('.chat-close').addEventListener('click', closeChat);
panel.addEventListener('keydown', (event) => {
  // IMEは変換の取り消しにEscapeキーを使うため、そのキーは見送る。
  if (event.isComposing || event.keyCode === 229) return;
  if (event.key === 'Escape') closeChat();
});
```

`div` のクリック処理は、マウスでしか動きません。×は `button` ですが、中にはタイトルのない `svg` しかないため、名前がありません。`closeChat()` の `launcher.focus()` も `div` には効かず、フォーカスは `body` に戻ってしまいます。

## 修正後：丸いアイコンのまま、名前のあるボタンにする

```html
<button type="button" class="chat-launcher" aria-label="チャットで相談">
  <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
    <use href="#icon-chat"></use>
  </svg>
</button>
<section id="chat" class="chat-panel" aria-labelledby="chat-title" hidden>
  <div class="chat-header">
    <h2 id="chat-title">Birchwoodのチャット</h2>
    <button type="button" class="chat-close" aria-label="チャットを閉じる">
      <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
        <use href="#icon-close"></use>
      </svg>
    </button>
  </div>
  <div class="chat-messages" role="log" aria-label="メッセージ">
    <p class="message from-us">こんにちは。ご注文についてお気軽にお尋ねください。</p>
  </div>
  <form class="chat-form">
    <label for="chat-input" class="visually-hidden">メッセージ</label>
    <input id="chat-input" autocomplete="off">
    <button>送信</button>
  </form>
</section>
```

```js
const launcher = document.querySelector('.chat-launcher');
const panel = document.getElementById('chat');
const input = document.getElementById('chat-input');

// パネルは起動ボタンの場所に開く。
function openChat() {
  launcher.hidden = true;
  panel.hidden = false;
  input.focus();
}
function closeChat() {
  panel.hidden = true;
  launcher.hidden = false;
  launcher.focus();
}

launcher.addEventListener('click', openChat);
panel.querySelector('.chat-close').addEventListener('click', closeChat);
panel.addEventListener('keydown', (event) => {
  // IMEは変換の取り消しにEscapeキーを使うため、そのキーは見送る。
  if (event.isComposing || event.keyCode === 229) return;
  if (event.key === 'Escape') closeChat();
});
```

起動ボタンは `button` になったので、Tabキーで届き、EnterキーでもSpaceキーでも押せます。押すと同じクリック処理が動き、チャットを閉じればフォーカスは起動ボタンに戻ります。`aria-label` で、起動ボタンに「チャットで相談」、×に「チャットを閉じる」という名前を付けたので、「チャットで相談、ボタン」「チャットを閉じる、ボタン」と読まれます。`aria-hidden` で、アイコンは読み上げの対象から外れます。スクリプトは変わりません。ボタンには `div` のときと同じクラスを付けているので、見た目もそのままです。ブラウザーの既定の枠線や背景が出る場合は、そのクラスで消します。各ボタンの中に視覚的に隠したテキストを入れても、同じように名前を付けられます。[名前がないアイコンだけのボタン](/ja/learn/icon-buttons)を参照してください。

## 修正後：起動ボタンに名前を表示する

```html
<button type="button" class="chat-launcher">
  <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
    <use href="#icon-chat"></use>
  </svg>
  チャットで相談
</button>
<section id="chat" class="chat-panel" aria-labelledby="chat-title" hidden>
  <div class="chat-header">
    <h2 id="chat-title">Birchwoodのチャット</h2>
    <button type="button" class="chat-close" aria-label="チャットを閉じる">
      <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
        <use href="#icon-close"></use>
      </svg>
    </button>
  </div>
  <div class="chat-messages" role="log" aria-label="メッセージ">
    <p class="message from-us">こんにちは。ご注文についてお気軽にお尋ねください。</p>
  </div>
  <form class="chat-form">
    <label for="chat-input" class="visually-hidden">メッセージ</label>
    <input id="chat-input" autocomplete="off">
    <button>送信</button>
  </form>
</section>
```

```js
const launcher = document.querySelector('.chat-launcher');
const panel = document.getElementById('chat');
const input = document.getElementById('chat-input');

// パネルは起動ボタンの場所に開く。
function openChat() {
  launcher.hidden = true;
  panel.hidden = false;
  input.focus();
}
function closeChat() {
  panel.hidden = true;
  launcher.hidden = false;
  launcher.focus();
}

launcher.addEventListener('click', openChat);
panel.querySelector('.chat-close').addEventListener('click', closeChat);
panel.addEventListener('keydown', (event) => {
  // IMEは変換の取り消しにEscapeキーを使うため、そのキーは見送る。
  if (event.isComposing || event.keyCode === 229) return;
  if (event.key === 'Escape') closeChat();
});
```

起動ボタンは、アイコンの横に「チャットで相談」と書いた横長のボタンになります。この言葉が名前になるため、「チャットで相談、ボタン」と読まれ、音声操作の利用者は見えている言葉をそのまま言えます。吹き出しの形を見てもチャットだと分からない人も、言葉を読めば分かります。×はアイコンのまま `aria-label` で「チャットを閉じる」と名付けます。パネルの隅の×は広く知られた形で、ヘッダーには場所もあまりないからです。

## どの方法を選ぶか

どちらも、起動ボタンと×をTabキーで届くボタンにし、名前を付けるため、2.1.1と4.1.2を満たします。名前を隠す方法なら、丸い吹き出しのデザインをそのまま残せ、起動ボタンがページに重なるスマートフォンでも場所を取りません。名前を表示する方法は幅を取りますが、何のボタンかが誰にでも分かります。音声操作の利用者は隠れた名前を推測せずにボタンの言葉を言え、アイコンを知らない人は言葉を読めます。デザインに余裕があれば名前を表示し、吹き出しを小さく保つ必要があれば名前を隠す方法を選んでください。

## チャットが外部サービスのものである場合

多くのチャットウィジェットは提供元のスクリプトで、起動ボタンもパネルも、提供元のドメインのフレームの中に描かれることがよくあります。その場合、自社のCSSやスクリプトは中に届きません。できることは次のとおりです。

- 導入する前に、「修正の確認」の手順でウィジェットを試す。提供元のデモページか、自社のテスト用ページで行います。
- ウィジェットの設定を確認する。起動ボタンに文字のラベルを表示できるものや、ボタンの名前を設定できるものがあります。サイトで使うそれぞれの言語で、両方を設定します。
- 起動ボタンと閉じるボタンが名前のあるHTML標準のボタンか、どのスクリーンリーダーで試したかを、提供元に尋ねる。フレームのタイトルについても尋ねます（詳しくは[タイトルがない、または中身の分からないiframe](/ja/learn/frame-titles)を参照）。
- うまくいかない点を、再現手順を添えて提供元に報告する。直るまでは、[アクセシビリティ方針](/ja/learn/accessibility-statement)に既知の問題として記載し、チャットを案内しているページに、メールアドレスなど別の問い合わせ方法を示します。

提供元のスクリプトは、自社のリリースと関係なく変わります。提供元の更新やウィジェットの設定の変更のたびに、確認を繰り返してください。

## 実装の判断

**divに `role="button"` を付けるだけでは足りない。** 役割を付ければスクリーンリーダーは「ボタン」と読みますが、`div` のままではフォーカスを受け取らず、EnterキーにもSpaceキーにも反応しません。`tabindex="0"` と、両方のキーを扱う処理を自分で加える必要があります。HTML標準の `button` には、どれも最初から備わっています。役割だけでは修正にならない理由は、[Tabキーで到達できないdivのボタン](/ja/learn/native-buttons)を参照してください。

**何をするかで名付ける。** 「チャットで相談」「ヘルプ」「質問する」は、押すと何が起きるかを伝えます。「チャットアイコン」「吹き出し」や提供元の製品名は伝えません。×は「チャットを閉じる」にし、「×」や「閉じるアイコン」にはしません。未読の返信の数を起動ボタンに表示する場合は、「チャットで相談、新着メッセージ1件」のように名前にも入れます。[メッセージを読み上げないチャットウィジェット](/ja/learn/chat-widgets)が、パネルを閉じている間に届いた返信について勧めている方法です。

**最小化ボタンもあるパネル。** 「最小化」と「閉じる」の二つのボタンを持つパネルもあります。名前のないアイコンが二つ並ぶと一つのとき以上に分からないため、それぞれ「チャットを最小化」「チャットを終了」のように、何をするかで名付けます。

**起動ボタンの位置。** 隅に固定された起動ボタンは、フォーカス中のコントロールを覆うことがあります。[フォーカス中のボタンを一部隠すチャットボタン](/ja/learn/focus-not-covered)を参照してください。HTMLでフッターの後に置かれたパネルには、フォーカスが最後にしか届きません。[フォーカス順の最後に回る、フローティングボタンとパネル](/ja/learn/floating-order)を参照してください。フォーカスを中に留めたまま、Tabキーで届く閉じるボタンもEscapeキーもなく、キーボードでは出られないパネルは、キーボードトラップです。[キーボードで閉じられないポップアップ](/ja/learn/closing-pop-ups)を参照してください。

## 修正の確認

1. マウスを使わずに、Tabキーでページを下へ進む。Tabキーがページを離れる前に、フォーカスの枠が見える状態で起動ボタンに届くことを確認する。
2. 起動ボタンでEnterキーを押し、別の回でSpaceキーを押す。チャットが開き、フォーカスがメッセージ欄にあることを確認する。
3. スクリーンリーダーを起動し、Tabキーで起動ボタンとパネルの×へ移る。「チャットで相談、ボタン」「チャットを閉じる、ボタン」と読まれることを確認する。
4. ×でEnterキーを押す。チャットが閉じ、フォーカスが起動ボタンに戻ることを確認する。もう一度開いてEscapeキーを押し、同じになることを確認する。
5. 音声操作で「チャットで相談をクリック」と言い、チャットが開くことを確認する。「チャットを閉じるをクリック」と言い、閉じることを確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。チャットウィジェット、その提供元のスクリプト、または設定を変更した際は、再度確認します。

## 制限

このサンプルは、ページ自身が描く起動ボタンとパネルのための出発点で、あらゆるウィジェットで同じ結果になることを保証するものではありません。提供元のウィジェットをそもそも変えられるかどうかは提供元によります。別のドメインのフレームの中にある場合、手を加えられるのは、ウィジェットの設定か、提供元への依頼だけです。チャットのメッセージの読み上げは別の問題で、[メッセージを読み上げないチャットウィジェット](/ja/learn/chat-widgets)で扱っています。ボットだけで担当者に連絡できないチャットについては、[担当者につながらないサポートのチャットボット](/ja/learn/human-help)を参照してください。実際に利用者が使う支援技術で、操作全体を確認してください。

---

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