# 場所が地図でしか分からない店舗マップ

Source: https://easeweb.dev/learn/interactive-maps
Topics: ドラッグ＆ドロップ > 地図にテキストの代替があり、キーボードで操作できる (WCAG 1.1.1, 2.1.1, 2.5.7)
The fix: 地図に示した場所を、名前、住所、目印、営業時間と一緒に、地図のそばに一覧で書きます。そのうえで、地図を残す場合は、ピンを場所の名前を持つボタンにして、地図を動かすボタンを加えます。地図を残さない場合は、すべての場所が収まる静止画像に替え、一覧に行き方のリンクを付けます。一覧だけでは、地図はドラッグでしか動かないままです。
Test with: keyboard, screen reader, mouse, touch, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html https://www.w3.org/WAI/tutorials/images/complex/ https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F108 https://leafletjs.com/reference.html

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

## 場面

コーヒー店の「カフェを探す」ページには、見出しと、町に3つのカフェがあるという一文と、地図があります。地図はこのデザインの主役で、町を描いた絵の上に、カフェごとにピンが立っています。見えているピンは、港のそばと市場広場の2つです。古い水車小屋のカフェは右端の外にあり、地図をドラッグすると見えてきます。ピンをクリックすると、地図の下のほうに小さなカードが開き、そのカフェの住所と営業時間が出ます。

Tabキーを押すと、フォーカスはページの先頭から、地図の下の「祝日の営業時間」のリンクへ一気に移ります。ピンには一度も止まらないので、カードは開かず、地図を動かす方法もありません。スクリーンリーダーでページを読み進めると、「カフェを探す、見出しレベル2」「町に3つのカフェがあります。」に続いてすぐにリンクが読まれます。どのカフェがどこにあるのかは、一度も読まれません。

## 確認できる問題

この地図は、カフェがどこにあるかという情報を伝える画像です。ところが画像は `alt=""` で、支援技術には装飾として扱われ、その上のピンは空の `div` です。カフェについてのテキストは、ピンをクリックしたときにスクリプトがカードに書き込むものしかありません。地図が伝える情報にテキストの代替がないため、1.1.1を満たしません。WAIの画像のチュートリアルは、場所を示す地図を「複雑な画像」に数えています。短い代替テキストで何の画像かを伝え、画像が示す内容はより長いテキストで、できればページの本文に書く、という扱いです。

ピンはクリックのハンドラーを持つ `div` です。ブラウザーは `div` をTabで止めず、キー操作も受け付けないため、キーボードではカフェのカードを開けません。地図の表示もドラッグするポインターでしか動かず、動かすキーもありません。カフェの詳しい情報を見ることも、端から外れた町の部分を見ることもページの機能ですが、どちらもキーボードではできないため、2.1.1を満たしません。

地図を動かす方法はドラッグだけなので、2.5.7も満たしません。地図の表示を動かすのに、ドラッグは必要ありません。ポインターがどんな道筋をたどったかで、結果は変わらないからです。2.5.7の解説書は、満たす例の一つとして、まさにこの場面を挙げています。地図の表示をドラッグで動かせ、上下左右のボタンでも動かせる、という例です。解説書は、キーボードで同じ操作ができるだけでは、この達成基準を満たすとは限らないとも書いています。タッチスクリーンやヘッドポインターを使う人、マウスのボタンを押したままにできない人には、クリックやタップで操作できるものが必要です。代わりの方法は地図の一部でなくてもよく、機能が同等であればかまいません。そのため、カフェの一覧は、カフェを見つけるための代わりにはなりますが、カフェの周りの道を見るための代わりにはなりません。

## 利用者への影響

スマートフォンでは、地図が画面いっぱいに広がっても映る町の範囲はさらに狭く、最初から端の外にあるカフェが増えます。カフェを探すページは出かける途中で開かれることも多く、そのとき言葉で書かれた住所は、自分の地図アプリに貼り付けて使うものでもあります。

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

自動チェックに見えるのは、`alt=""` の画像と3つの空の `div` です。名前が要るものも、コントロールを名乗るものもないため、修正前のコードは自動チェックのどのルールにも反しません。EaseWebの診断は、Leaflet、Mapbox GL、MapLibre、Google Maps、OpenLayersといった一般的な地図ライブラリーで描かれた地図と、地図サービスから埋め込まれた地図について、1つずつ確認を促す項目を出します。項目には、名前のないピンや「Marker」のようなライブラリーの汎用の名前のままのピンの数と、フォーカスを受け取らないピンの数が示されます。場所がページにも書かれているか、地図がドラッグせずに動くかは、人が見ないと分からないからです。この例のようにサイト独自のコードで描いた地図には、地図だと分かる目印がないため、診断では見つからず、人が確認する必要があります。AIのコーディング支援に地図のアクセシビリティ改善を頼むと、地図のコンテナーに `aria-label="地図"` を、ピンに `role="button"` と `tabindex="0"` を付けることがよくあります。ピンには届くようになりますが、どれも名前のないボタンとして読まれ、場所はやはりどこにも書かれず、地図もドラッグでしか動きません。

## 修正前：カフェが地図の中にしかない

```html
<section class="find-us" aria-labelledby="find-title">
  <h2 id="find-title">カフェを探す</h2>
  <p>町に3つのカフェがあります。</p>
  <div class="map" id="map">
    <div class="map-view">
      <img class="map-tiles" src="/maps/town.png" alt="">
      <div class="pin" data-shop="harbour" style="left: 20%; top: 50%"></div>
      <div class="pin" data-shop="market" style="left: 42%; top: 58%"></div>
      <div class="pin" data-shop="mill" style="left: 84%; top: 30%"></div>
    </div>
    <div class="map-card" id="map-card" hidden></div>
  </div>
  <a href="/holiday-hours">祝日の営業時間</a>
</section>
```

```js
const map = document.getElementById('map');
const view = map.querySelector('.map-view');
const card = document.getElementById('map-card');
const SHOPS = {
  harbour: 'Harbour Street：12 Harbour Street（フェリー乗り場のそば）。毎日 7:00–18:00。',
  market: 'Market Square：3 Market Square（市役所の向かい）。毎日 8:00–17:00。',
  mill: 'Old Mill：The Old Mill, Mill Lane（川沿い）。土日 9:00–16:00。'
};
view.addEventListener('pointerdown', startPan);
// pointermoveで.map-viewをポインターに合わせて動かし、pointerupで終える
for (const pin of map.querySelectorAll('.pin')) {
  pin.addEventListener('click', () => {
    card.textContent = SHOPS[pin.dataset.shop];
    card.hidden = false;
  });
}
```

- 画像は装飾とされています。
- ピンは空です。
- 住所はクリックでカードに入るまでスクリプトの中にしかなく、カフェはページのどこにも書かれていません。
- ピンは、フォーカスもキー操作も受け取りません。
- 地図はポインターでドラッグしている間しか動かないため、ドラッグしなければOld Millは見つかりません。

## 修正後：カフェの一覧、ボタンにしたピン、地図を動かすボタン

```html
<section class="find-us" aria-labelledby="find-title">
  <h2 id="find-title">カフェを探す</h2>
  <p>町に3つのカフェがあります。</p>
  <div class="map" id="map">
    <div class="map-view">
      <img class="map-tiles" src="/maps/town.png" alt="">
      <button type="button" class="pin" data-shop="harbour" style="left: 20%; top: 50%" aria-label="Harbour Street"></button>
      <button type="button" class="pin" data-shop="market" style="left: 42%; top: 58%" aria-label="Market Square"></button>
      <button type="button" class="pin" data-shop="mill" style="left: 84%; top: 30%" aria-label="Old Mill"></button>
    </div>
    <div class="map-card" id="map-card" hidden></div>
    <div class="map-pan" role="group" aria-label="地図を動かす">
      <button type="button" data-pan="up" aria-label="上">↑</button>
      <button type="button" data-pan="left" aria-label="左">←</button>
      <button type="button" data-pan="right" aria-label="右">→</button>
      <button type="button" data-pan="down" aria-label="下">↓</button>
    </div>
  </div>
  <ul class="cafes">
    <li>
      <h3>Harbour Street</h3>
      <p>12 Harbour Street（フェリー乗り場のそば）。毎日 7:00–18:00。</p>
      <a href="https://maps.example/?q=12+Harbour+Street">Harbour Streetへの行き方</a>
    </li>
    <li>
      <h3>Market Square</h3>
      <p>3 Market Square（市役所の向かい）。毎日 8:00–17:00。</p>
      <a href="https://maps.example/?q=3+Market+Square">Market Squareへの行き方</a>
    </li>
    <li>
      <h3>Old Mill</h3>
      <p>The Old Mill, Mill Lane（川沿い）。土日 9:00–16:00。</p>
      <a href="https://maps.example/?q=The+Old+Mill+Mill+Lane">Old Millへの行き方</a>
    </li>
  </ul>
  <a href="/holiday-hours">祝日の営業時間</a>
</section>
```

```js
const map = document.getElementById('map');
const view = map.querySelector('.map-view');
const card = document.getElementById('map-card');
const SHOPS = {
  harbour: 'Harbour Street：12 Harbour Street（フェリー乗り場のそば）。毎日 7:00–18:00。',
  market: 'Market Square：3 Market Square（市役所の向かい）。毎日 8:00–17:00。',
  mill: 'Old Mill：The Old Mill, Mill Lane（川沿い）。土日 9:00–16:00。'
};
let [x, y] = [0, 0];
// ドラッグとボタンのどちらで地図を動かしても、ここを通る
function moveTo(toX, toY) {
  [x, y] = [toX, toY]; // 地図の端からはみ出さないように収める
  view.style.translate = `${x}px ${y}px`;
}
const STEPS = { up: [0, 100], down: [0, -100], left: [100, 0], right: [-100, 0] };
for (const button of map.querySelectorAll('[data-pan]')) {
  button.addEventListener('click', () => {
    const [dx, dy] = STEPS[button.dataset.pan];
    moveTo(x + dx, y + dy);
  });
}
view.addEventListener('pointerdown', startPan); // ドラッグ中もmoveTo()を呼ぶ
for (const pin of map.querySelectorAll('.pin')) {
  pin.addEventListener('click', () => {
    card.textContent = SHOPS[pin.dataset.shop];
    card.hidden = false;
  });
  // キーボードで移ったピンが表示の中央に来るよう地図を動かし、枠が端の外に隠れないようにする
  pin.addEventListener('focus', () => {
    if (pin.matches(':focus-visible')) moveTo(map.clientWidth / 2 - pin.offsetLeft, map.clientHeight / 2 - pin.offsetTop);
  });
}
```

1. 地図の下に、テキストの代替としてカフェの一覧を書きます。カフェごとに、名前を見出しにし、住所、地図の上でどこにあるかが分かる目印（「フェリー乗り場のそば」）、営業時間、行き方のリンクを書きます。町を知らない人には住所だけでは地図が示すことが伝わらないため、目印も代替の一部です。
2. 地図の画像は `alt=""` のままにします。画像が示すことは、そのすぐ下に書いてあるからです。
3. ピンを、カフェの名前を持つHTML標準のボタンにします。Tabキーでそれぞれに止まり、EnterキーやSpaceキーでカードが開き、音声操作では「Old Millをクリック」と言えます。
4. 「上」「下」「左」「右」のボタンを「地図を動かす」という名前のグループにまとめて置き、それぞれ表示が動く向きを名前にします。1回のクリックやタップで表示を100ピクセルずつ動かします。短くドラッグしたときと同じくらいの距離で、「右」を何回か押せば、Old Millが見えてきます。
5. ボタンもドラッグも、同じ `moveTo()` を通します。ドラッグは近道として残ります。
6. Tabキーでピンに移ったら、そのピンが表示の中央に来るように地図を動かします。Tabキーで移動しても、フォーカスの枠が端の外に隠れることはありません。この処理がないと、ブラウザーが地図の枠そのものをピンまでスクロールしてしまい、地図の実際の位置が、スクリプトが把握している位置とずれます。

これで修正前の例は直り、1.1.1、2.1.1、2.5.7を満たします。

## 修正後：カフェの一覧と、すべてが収まる静止画像の地図

```html
<section class="find-us" aria-labelledby="find-title">
  <h2 id="find-title">カフェを探す</h2>
  <p>町に3つのカフェがあります。</p>
  <img class="map-still" src="/maps/three-cafes.png" alt="3つのカフェの地図（場所は下の一覧を参照）">
  <ul class="cafes">
    <li>
      <h3>Harbour Street</h3>
      <p>12 Harbour Street（フェリー乗り場のそば）。毎日 7:00–18:00。</p>
      <a href="https://maps.example/?q=12+Harbour+Street">Harbour Streetへの行き方</a>
    </li>
    <li>
      <h3>Market Square</h3>
      <p>3 Market Square（市役所の向かい）。毎日 8:00–17:00。</p>
      <a href="https://maps.example/?q=3+Market+Square">Market Squareへの行き方</a>
    </li>
    <li>
      <h3>Old Mill</h3>
      <p>The Old Mill, Mill Lane（川沿い）。土日 9:00–16:00。</p>
      <a href="https://maps.example/?q=The+Old+Mill+Mill+Lane">Old Millへの行き方</a>
    </li>
  </ul>
  <a href="/holiday-hours">祝日の営業時間</a>
</section>
```

```js
// 地図は画像になったため、動かす操作も、開くピンもない
```

1. 操作できる地図をやめ、町全体を描いた画像に替えます。3つのカフェがすべて収まるところまで引いて描き、ピンの横に名前を添えます。ページの中に動くものがないので、ドラッグするものも、地図の中でキーボードで操作するものもありません。
2. 代替テキストでは、何の画像かと、その情報がどこに書かれているかを伝えます。WAIの画像のチュートリアルが複雑な画像について挙げる方法の一つです。
3. 画像の下にカフェの一覧を書き、残りはそこで伝えます。行き方のリンクは道順を地図サービスに任せます。スマートフォンでは、利用者の地図アプリで開くこともあります。

これで修正前の例は直り、1.1.1、2.1.1、2.5.7を満たします。3つの修正の中でコードが最も少なく、場所の数が少なく1枚の絵に収まるページに向いています。場所が多いときや、町が広くて読める大きさで描けないときは、操作できる地図が役に立ちます。

## 修正後：カフェの一覧だけ（地図はそのまま）

```html
<section class="find-us" aria-labelledby="find-title">
  <h2 id="find-title">カフェを探す</h2>
  <p>町に3つのカフェがあります。</p>
  <div class="map" id="map">
    <div class="map-view">
      <img class="map-tiles" src="/maps/town.png" alt="">
      <div class="pin" data-shop="harbour" style="left: 20%; top: 50%"></div>
      <div class="pin" data-shop="market" style="left: 42%; top: 58%"></div>
      <div class="pin" data-shop="mill" style="left: 84%; top: 30%"></div>
    </div>
    <div class="map-card" id="map-card" hidden></div>
  </div>
  <ul class="cafes">
    <li>
      <h3>Harbour Street</h3>
      <p>12 Harbour Street（フェリー乗り場のそば）。毎日 7:00–18:00。</p>
      <a href="https://maps.example/?q=12+Harbour+Street">Harbour Streetへの行き方</a>
    </li>
    <li>
      <h3>Market Square</h3>
      <p>3 Market Square（市役所の向かい）。毎日 8:00–17:00。</p>
      <a href="https://maps.example/?q=3+Market+Square">Market Squareへの行き方</a>
    </li>
    <li>
      <h3>Old Mill</h3>
      <p>The Old Mill, Mill Lane（川沿い）。土日 9:00–16:00。</p>
      <a href="https://maps.example/?q=The+Old+Mill+Mill+Lane">Old Millへの行き方</a>
    </li>
  </ul>
  <a href="/holiday-hours">祝日の営業時間</a>
</section>
```

```js
const map = document.getElementById('map');
const view = map.querySelector('.map-view');
const card = document.getElementById('map-card');
const SHOPS = {
  harbour: 'Harbour Street：12 Harbour Street（フェリー乗り場のそば）。毎日 7:00–18:00。',
  market: 'Market Square：3 Market Square（市役所の向かい）。毎日 8:00–17:00。',
  mill: 'Old Mill：The Old Mill, Mill Lane（川沿い）。土日 9:00–16:00。'
};
view.addEventListener('pointerdown', startPan);
// pointermoveで.map-viewをポインターに合わせて動かし、pointerupで終える
for (const pin of map.querySelectorAll('.pin')) {
  pin.addEventListener('click', () => {
    card.textContent = SHOPS[pin.dataset.shop];
    card.hidden = false;
  });
}
```

1. 地図の下に最初の修正と同じ一覧を加え、地図にはほかに手を付けません。どのカフェも、どこにあるかと一緒にページに書かれ、スクリーンリーダーですべて読まれ、一覧の中でキーボードでも1回のクリックでも見つけられます。

最も小さな変更で、1.1.1は満たします。しかし地図そのものはドラッグでしか動かず、ピンもフォーカスを受け取らないままです。キーボードの利用者やドラッグできない人は、カフェの周りの道を見ることも、Old Millを表示させることもできません。住所の一覧はその代わりにはならないため、地図については2.1.1と2.5.7を満たしません。これだけでは十分ではありません。最初の修正のようにピンのボタンと地図を動かすボタンを加えるか、2つ目の修正のように地図を静止画像に替えてください。

## どの方法を選ぶか

最初の2つの修正は、どちらも3つの達成基準を満たします。どちらを選ぶかはページによります。カフェの近くに何があるかを見たり、多くの場所の位置を比べたりと、地図そのものが使われるなら、一覧、ピンのボタン、地図を動かすボタンを備えた操作できる地図を残します。コードは最も多くなり、サイトで使う地図のコンポーネントすべてについて、同じ3点を確認する必要があります。1つの町の3つのカフェのように、場所が少なく1枚の絵に収まるなら、静止画像の地図を使います。コードが最も少なく、キーボードで操作できるようにするものもなく、行き方のリンクから、道順のために作られた地図アプリへ案内できます。地図をそのまま残して一覧だけを加える方法は、誰にでも住所が伝わる第一歩ですが、地図にはやはりボタンを加えるか、静止画像に替える必要があります。

## 実装の判断

一覧は、「リストで表示」のタブの奥や別のページではなく、同じページの地図のそばに、読む順序どおりに置きます。誰もが使えるのは一覧のほうです。実際のサイトでは、次の点で一覧と地図の動きが決まります。

- 順序：マークアップでは、ここでのように一覧を地図の後に置いても、前に置いてもかまわない。一覧のカフェとピンを同じ順序に並べ、スクリーンリーダーでページを読む人が、Tabで地図を通り抜けなくても一覧にたどり着けるようにする。
- ピンのカード：ピンでカードを開くなら、ここでのようにマークアップではピンのすぐ後にカードを置き、カードにリンクがあるときは、Tabでピンの次にカードへ進めるようにする。キーボードで操作するものがカードにないなら、一覧に同じことが書いてあるので、ただのテキストのままでかまわない。
- 場所が多い場合：場所が何十もあると、Tabで止まるピンが多くなりすぎる。一覧は残して、検索や地域での絞り込みを付け、ピンは一覧で代わりにする目印として描く。ピンが示す場所はすべて一覧にあるので、ピンはフォーカスを受け取らない要素にして `aria-hidden="true"` で支援技術から隠し、場所ごとのボタンは一覧に置く。フォーカスを受け取れるピンを隠してはいけない。クリックで開くピンのまとまり（クラスター）は1回のクリックで操作できるため、問題ない。
- 拡大と縮小：マウスホイールやピンチで地図を拡大するなら、地図を動かすボタンと同じ理由で、「拡大」「縮小」のボタンも加える。ピンチ全般は「[ボタンで代われないピンチ・ドラッグ・傾きのジェスチャー](/ja/learn/gestures)」で扱っている。
- 地図のキー操作：地図ライブラリーでは、地図そのものをTabで止まるようにし、矢印キーで動かし、+と−のキーで拡大・縮小できるようにしていることが多い。これで2.1.1は満たすが、2.5.7は満たさず、スクリーンリーダーは矢印キーを読み上げのために使うことも多いため、ボタンも用意する。
- フォーカス：ピンとボタンには、背後の地図に対して3:1以上のフォーカスの枠を付け、「Marker」ではなく場所に合った名前を付ける。詳しくは「[コントラストが足りない入力欄・アイコン・フォーカス枠](/ja/learn/non-text-contrast)」を参照。
- ドラッグ：ドラッグで動かす地図には、指で触れてもページがスクロールしないように、たいてい `touch-action: none` が付いている。スマートフォンで地図が横幅いっぱいに広がると、指でページをスクロールできなくなるため、地図の横にスクロールのための余白を残すか、`touch-action: pan-y` にして、上下のスワイプではページがスクロールし、地図の上下はボタンで動かせるようにする。

地図ライブラリーにはそれぞれ違いがあり、その初期設定で多くのことが決まります。たとえばLeafletでは、マーカーはそれぞれTabで止まり、Enterキーで押せますが、`alt` に場所の名前を設定しない限り、画像の代替テキストは「Marker」になります。地図そのものもフォーカスでき、矢印キーで動き、+と−で拡大・縮小でき、「拡大」「縮小」のボタンも描かれますが、地図を動かすボタンはありません。地図サービスから埋め込んだ地図の操作はサービス側のもので変えられないため、自分で手を入れられるのはページの一覧です。ライブラリーの説明ではなく、フォーカスしたピンが何と呼ばれるか、1回のクリックで何ができるかを確認してください。

## 修正の確認

1. スクリーンリーダーで、地図に触れずに見出しからページを読み、すべての場所の名前、住所、目印、営業時間が読まれること、静止画像の地図の代替テキストが、場所の書かれている箇所を伝えることを確認する。
2. マウスを使わずにTabキーでページを移動し、一覧の中で、また地図を残す場合はそれぞれのピンで、すべての場所に届くこと、ピンが場所の名前で読まれることを確認する。
3. 地図を残す場合は、キーボードだけで、地図のボタンを使って、最初の表示から最も遠い場所が見えるまで地図を動かし、途中のどのピンとボタンにもフォーカスの枠が見えることを確認する。
4. マウスのクリックだけで、またタッチスクリーンのタップだけで、地図のすべての部分を表示し、それぞれの場所の詳しい情報を、ドラッグせずに開く。音声操作では、ボタンの名前を言って地図を動かす。
5. スマートフォンと400%の拡大表示で、一覧が地図と一緒にあること、地図を動かすボタンが残っていること、地図に指で触れてもページのスクロールが止まらないことを確認する。

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

## 制限

このサンプルは、どのアプリケーションでも同じ結果になることを保証するものではありません。住所、目印、URLは仮のものです。このガイドは、場所を探す方法が地図しかない場合を扱います。地図のiframeのタイトルは「[タイトルがない、または中身の分からないiframe](/ja/learn/frame-titles)」、画像だけの地図の代替テキストは「[代替テキストがない、情報を伝える画像](/ja/learn/informative-images)」、ピンチ全般は「[ボタンで代われないピンチ・ドラッグ・傾きのジェスチャー](/ja/learn/gestures)」で扱っています。道順、ターンバイターンの案内、フロアマップ、利用者が書き込む地図には、それぞれ別の配慮が必要で、ここでは扱いません。地図サービスの埋め込みやアプリは自分では変えられません。行き方のリンクから移った先で利用者が出会うものを、対象とする支援技術で確認してください。

---

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