# マウスでしか切り替えも拡大もできない商品写真ギャラリー

Source: https://easeweb.dev/learn/image-galleries
Topics: 複雑なウィジェット > 商品写真のギャラリーをキーボードで切り替え・拡大でき、表示中の写真が伝わる (WCAG 2.1.1, 4.1.2)
The fix: 各サムネイルを、画像のalt属性で名前が付くHTML標準のボタンにし、表示中のものに aria-current を付けます。そのうえで、スクロールできる枠と拡大ボタン、大きな写真を出すHTML標準のダイアログ、元サイズの写真へのリンクのいずれかで、キーボードから細部を見られるようにします。
Test with: keyboard, screen reader, forced colors
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/WCAG22/Techniques/failures/F54 https://www.w3.org/TR/wai-aria-1.2/#aria-current https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-current https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog

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

## 場面

あるショップがホーローマグを売っています。商品ページには大きな写真が1枚あり、その下に正面、側面、背面の3つのサムネイルが並びます。背面には「Dishwasher safe」「350 ml」という小さな印字があり、この大きさでは読めません。マウスなら問題なく使えます。最後のサムネイルをクリックすると背面の写真に変わり、写真の上でマウスを動かすと、ポインターの周りが拡大されて印字が読めます。

マウスから手を離してTabキーを押してみます。フォーカスはそのまま「カートに入れる」へ移ります。サムネイルはクリック処理を付けた画像で、フォーカスを受けないため、キーボード利用者が見られるのはマグの正面だけです。拡大は写真の上でマウスを動かすと始まる仕組みで、キーボードでは始められません。正しい写真を出せたとしても、小さな印字は読めないままです。

サムネイルを本物のボタンにしたギャラリーもあります。Tabで届き、Enterキーで写真も変わります。ところが画像のaltが空なので、どのボタンも「ボタン」としか読まれません。表示中の印はクラスで描いた枠線だけで、スクリーンリーダーには伝わらず、押して写真が変わっても何も読まれません。拡大にはやはりマウスが必要です。

## 確認できる問題

達成基準2.1.1（キーボード）は、マウスでできることはすべてキーボードでもできるよう求めています。写真を切り替えるには画像をクリックしますが、画像はフォーカスを受けないので、Tabは止まらず、Enterキーで押す相手もありません。拡大は `mousemove` で始まりますが、このイベントはどのキーを押しても発生しません。W3Cの失敗例F54が挙げているのがまさにこの場合で、ポインティングデバイス専用のイベントハンドラーでしか機能を実行できない状態を指します。ここでの機能は、写真の細部を見ることです。

達成基準4.1.2（名前・役割・値）は、コントロールの名前、役割、状態を伝えるよう求めています。クリックできる画像にはボタンの役割がありません。`alt=""` の画像だけを入れたボタンには、役割はあっても名前がありません。さらにどちらも、表示中のサムネイルは枠線を描く `is-current` クラスでしか分からず、状態が伝わりません。画像をボタンにすればキーボードの問題は解決しますが、こちらは残ります。

サムネイルを押すと、フォーカスはサムネイルに残ったまま、ページの別の場所で写真が変わります。そのためスクリーンリーダー利用者には、何かが起きたことが伝わらない場合があります。どちらの基準も、この変化を読み上げることまでは求めていません。ただ、伝わるに越したことはないので、以下の修正では、カルーセルのガイドで新しいスライドを読み上げるのと同じように、控えめなライブリージョンで知らせます。

## 利用者への影響

背面、ラベル、お手入れの説明など、1枚目にはない情報がほかの写真に写っていることはよくあります。マウスを使えない人には正面の写真しか見えず、ほかの写真があることに気づかない場合もあります。

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

画像で作ったサムネイルにはボタンの役割がないので、axeには調べる対象がありません。この診断が報告するのは、クラス名からサムネイルと判断でき、カーソルがポインターになり、フォーカスを受けず、近くにずっと大きな写真があって、ほかに写真を変える手段がギャラリーにないものです。キーボードで押せる前後のボタンが並ぶサムネイルは対象外です。FlexSliderのサムネイルも対象外です。スライダーが1つだけのページなら、初期設定のままで、どこにいても左右の矢印キーでスライドが変わるためです。サムネイルが名前のないボタンなら、axeが `button-name` を報告します。`role="button"` を付けたdivなら `aria-command-name` です。

次の2つは人の判断が必要なため、確認の手がかりとして報告します。1つは、どのサムネイルにも状態がないボタンの列です。`aria-current` がないことはコードで分かっても、ページがほかの方法で伝えているかまでは分かりません。もう1つは、マウスでの拡大を設定した写真で（Drift、elevateZoom、jQuery Zoom、Magentoの拡大鏡、Dawnのホバー設定が残す目印で判断します）、写真がリンクの中になく、近くに拡大や写真を開くと分かるコントロールがない場合です。自前で実装した拡大にはこうした目印がなく、どのチェックでも見つかりません。

手早く直そうとして各画像に `tabindex="0"` を足すだけでは、Tabで止まっても、EnterキーもSpaceキーも効きません。フォーカスで拡大が始まるようにしても同じことで、見たい部分へ表示を動かす手段がキーボードにはありません。

## 修正前：マウスでしか使えないサムネイルと拡大

```html
<div class="gallery">
  <div class="photo-frame">
    <img class="photo" src="mug-front.jpg" alt="マグの正面">
  </div>
  <div class="thumbs">
    <img class="thumb is-current" src="mug-front-small.jpg" alt="" data-photo="mug-front.jpg" data-alt="マグの正面">
    <img class="thumb" src="mug-side-small.jpg" alt="" data-photo="mug-side.jpg" data-alt="マグの側面">
    <img class="thumb" src="mug-back-small.jpg" alt="" data-photo="mug-back.jpg" data-alt="マグの背面">
  </div>
</div>
```

```css
.photo-frame { aspect-ratio: 1; overflow: hidden; }
.photo { display: block; width: 100%; }
.thumb { border: 2px solid transparent; cursor: pointer; }
/* 表示中のサムネイルの枠線は、クラスで描いているだけ。 */
.thumb.is-current { border-color: #2d3a55; }
```

```js
const frame = document.querySelector('.photo-frame');
const photo = frame.querySelector('.photo');
const thumbs = [...document.querySelectorAll('.thumb')];
thumbs.forEach((thumb) => thumb.addEventListener('click', () => {
  photo.src = thumb.dataset.photo;
  photo.alt = thumb.dataset.alt;
  thumbs.forEach((other) => other.classList.toggle('is-current', other === thumb));
}));
// 拡大はマウスの動きに合わせて行う。
frame.addEventListener('mousemove', (event) => {
  const box = frame.getBoundingClientRect();
  const x = ((event.clientX - box.left) / box.width) * 100;
  const y = ((event.clientY - box.top) / box.height) * 100;
  photo.style.transformOrigin = `${x}% ${y}%`;
  photo.style.transform = 'scale(3)';
});
frame.addEventListener('mouseleave', () => { photo.style.transform = ''; });
```

- サムネイルは `click` にしか反応しない画像なので、Tabで止まらず、キーボードでは別の写真を表示できません。
- 役割がなく、altも空です。
- 表示中のサムネイルの印は `is-current` クラスだけで、枠線を描いているにすぎません。
- 拡大は `mousemove` でしか始まらないので、キーボードからは細部を見られません。
- 新しい写真に変わっても、スクリーンリーダーには何も読まれません。

## 修正前：何も伝えないサムネイルのボタン

```html
<div class="gallery">
  <div class="photo-frame">
    <img class="photo" src="mug-front.jpg" alt="マグの正面">
  </div>
  <div class="thumbs">
    <button type="button" class="thumb is-current" data-photo="mug-front.jpg" data-alt="マグの正面">
      <img src="mug-front-small.jpg" alt="">
    </button>
    <button type="button" class="thumb" data-photo="mug-side.jpg" data-alt="マグの側面">
      <img src="mug-side-small.jpg" alt="">
    </button>
    <button type="button" class="thumb" data-photo="mug-back.jpg" data-alt="マグの背面">
      <img src="mug-back-small.jpg" alt="">
    </button>
  </div>
</div>
```

```css
.photo-frame { aspect-ratio: 1; overflow: hidden; }
.photo { display: block; width: 100%; }
.thumb { border: 2px solid transparent; cursor: pointer; }
/* 表示中のサムネイルの枠線は、クラスで描いているだけ。 */
.thumb.is-current { border-color: #2d3a55; }
```

```js
const frame = document.querySelector('.photo-frame');
const photo = frame.querySelector('.photo');
const thumbs = [...document.querySelectorAll('.thumb')];
thumbs.forEach((thumb) => thumb.addEventListener('click', () => {
  photo.src = thumb.dataset.photo;
  photo.alt = thumb.dataset.alt;
  thumbs.forEach((other) => other.classList.toggle('is-current', other === thumb));
}));
// 拡大はマウスの動きに合わせて行う。
frame.addEventListener('mousemove', (event) => {
  const box = frame.getBoundingClientRect();
  const x = ((event.clientX - box.left) / box.width) * 100;
  const y = ((event.clientY - box.top) / box.height) * 100;
  photo.style.transformOrigin = `${x}% ${y}%`;
  photo.style.transform = 'scale(3)';
});
frame.addEventListener('mouseleave', () => { photo.style.transform = ''; });
```

Tabですべてのサムネイルに届き、EnterキーとSpaceキーで押せるので、キーボードでどの写真も表示できます。しかし、ほかは変わっていません。

- 中身が `alt=""` の画像だけなので、どのボタンにも名前がなく、4.1.2を満たしません。
- 表示中のサムネイルの印は `is-current` だけのままで、どの写真を表示中かがスクリーンリーダーに伝わりません。
- 拡大は `mousemove` でしか始まらないままで、2.1.1を満たしません。
- 新しい写真に変わっても、スクリーンリーダーには何も読まれません。

## 修正後：拡大ボタンとスクロールできる枠

```html
<div class="gallery">
  <button type="button" class="zoom">拡大</button>
  <div class="photo-frame" role="region" aria-label="拡大した写真">
    <img class="photo" src="mug-front.jpg" alt="マグの正面">
  </div>
  <p class="photo-count" aria-live="polite">写真3枚中1枚目：マグの正面</p>
  <div class="thumbs">
    <button type="button" class="thumb" aria-current="true" data-photo="mug-front.jpg">
      <img src="mug-front-small.jpg" alt="マグの正面">
    </button>
    <button type="button" class="thumb" data-photo="mug-side.jpg">
      <img src="mug-side-small.jpg" alt="マグの側面">
    </button>
    <button type="button" class="thumb" data-photo="mug-back.jpg">
      <img src="mug-back-small.jpg" alt="マグの背面">
    </button>
  </div>
</div>
```

```css
.photo-frame { aspect-ratio: 1; overflow: hidden; }
.photo { display: block; width: 100%; }
.thumb { border: 2px solid transparent; cursor: pointer; }
/* 表示中のサムネイルは、スクリーンリーダーが読む属性から描く。 */
.thumb[aria-current='true'] { border-color: #2d3a55; }
/* コントラスト テーマではすべての枠線が同じシステム色になるため、表示中のものだけ別の色にする */
@media (forced-colors: active) {
  .thumb[aria-current='true'] { border-color: Highlight; }
}
/* 拡大すると、写真はブラウザーがスクロールし、矢印キーでも動かせる。 */
.photo-frame.is-zoomed { overflow: auto; }
.photo-frame.is-zoomed .photo { width: 300%; max-width: none; }
```

```js
const frame = document.querySelector('.photo-frame');
const photo = frame.querySelector('.photo');
const thumbs = [...document.querySelectorAll('.thumb')];
const count = document.querySelector('.photo-count');
const zoom = document.querySelector('.zoom');
thumbs.forEach((thumb, i) => thumb.addEventListener('click', () => {
  photo.src = thumb.dataset.photo;
  photo.alt = thumb.querySelector('img').alt;
  count.textContent = `写真${thumbs.length}枚中${i + 1}枚目：${photo.alt}`;
  thumbs.forEach((other) =>
    other === thumb ? other.setAttribute('aria-current', 'true') : other.removeAttribute('aria-current'));
}));
// マウスの動きに合わせた拡大は、近道として残す。ボタンで拡大している間は止める。
frame.addEventListener('mousemove', (event) => {
  if (frame.classList.contains('is-zoomed')) return;
  const box = frame.getBoundingClientRect();
  const x = ((event.clientX - box.left) / box.width) * 100;
  const y = ((event.clientY - box.top) / box.height) * 100;
  photo.style.transformOrigin = `${x}% ${y}%`;
  photo.style.transform = 'scale(3)';
});
frame.addEventListener('mouseleave', () => { photo.style.transform = ''; });
// ボタンで枠の中の写真を大きくし、写真の中央から見せる。
zoom.addEventListener('click', () => {
  const zoomed = frame.classList.toggle('is-zoomed');
  photo.style.transform = '';
  if (zoomed) frame.tabIndex = 0;
  else frame.removeAttribute('tabindex');
  zoom.textContent = zoomed ? '縮小' : '拡大';
  frame.scrollTo((frame.scrollWidth - frame.clientWidth) / 2, (frame.scrollHeight - frame.clientHeight) / 2);
});
```

1. 各サムネイルを、画像を中に入れた本物のボタンにします。Tabで届き、EnterキーとSpaceキーでも押せるので、キー操作の処理を書く必要はありません。
2. 各サムネイルのaltに、どの写真かを書きます。ボタンの中身が画像だけなら、このテキストがボタンの名前になります。
3. クリック処理の中で、表示中のサムネイルに `aria-current="true"` を付けてほかから外し、スクリーンリーダーが「現在の項目」と読めるようにします。
4. 枠線を `[aria-current='true']` から描きます。これで見た目と読み上げが食い違いません。
5. 必須ではありませんが、どの写真を表示中かを控えめなライブリージョンで伝えます。フォーカスを動かさなくても、押した結果が耳で分かります。
6. 「拡大」ボタンを足します。スクロールできる枠の中で写真の幅を3倍にし、枠をTabの移動先にするので、矢印キーで写真の中を動けます。Tabで先に届くよう、ボタンは枠より前に置きます。
7. マウスの動きに合わせた拡大は近道として残し、ボタンで拡大している間は止めます。

これで修正前の2つの問題がどちらも解消し、2.1.1と4.1.2を満たします。幅を3倍にしても細部が鮮明なように、十分に大きな画像ファイルを用意してください。

## 修正後：大きな写真をHTML標準のダイアログで見せる

```html
<div class="gallery">
  <button type="button" class="enlarge">写真を拡大</button>
  <div class="photo-frame">
    <img class="photo" src="mug-front.jpg" alt="マグの正面" data-large="mug-front-large.jpg">
  </div>
  <p class="photo-count" aria-live="polite">写真3枚中1枚目：マグの正面</p>
  <div class="thumbs">
    <button type="button" class="thumb" aria-current="true" data-photo="mug-front.jpg" data-large="mug-front-large.jpg">
      <img src="mug-front-small.jpg" alt="マグの正面">
    </button>
    <button type="button" class="thumb" data-photo="mug-side.jpg" data-large="mug-side-large.jpg">
      <img src="mug-side-small.jpg" alt="マグの側面">
    </button>
    <button type="button" class="thumb" data-photo="mug-back.jpg" data-large="mug-back-large.jpg">
      <img src="mug-back-small.jpg" alt="マグの背面">
    </button>
  </div>
  <dialog class="large-view" aria-label="拡大した写真">
    <form method="dialog">
      <button>閉じる</button>
    </form>
    <div class="large-frame" tabindex="0" role="region" aria-label="大きな写真">
      <img src="" alt="">
    </div>
  </dialog>
</div>
```

```css
.photo-frame { aspect-ratio: 1; overflow: hidden; }
.photo { display: block; width: 100%; }
.thumb { border: 2px solid transparent; cursor: pointer; }
/* 表示中のサムネイルは、スクリーンリーダーが読む属性から描く。 */
.thumb[aria-current='true'] { border-color: #2d3a55; }
/* コントラスト テーマではすべての枠線が同じシステム色になるため、表示中のものだけ別の色にする */
@media (forced-colors: active) {
  .thumb[aria-current='true'] { border-color: Highlight; }
}
/* 枠より大きな写真はスクロールでき、矢印キーでも動かせる。 */
.large-frame { max-width: 80vw; max-height: 70vh; overflow: auto; }
.large-frame img { display: block; }
```

```js
const frame = document.querySelector('.photo-frame');
const photo = frame.querySelector('.photo');
const thumbs = [...document.querySelectorAll('.thumb')];
const count = document.querySelector('.photo-count');
const dialog = document.querySelector('.large-view');
const large = dialog.querySelector('img');
thumbs.forEach((thumb, i) => thumb.addEventListener('click', () => {
  photo.src = thumb.dataset.photo;
  photo.alt = thumb.querySelector('img').alt;
  photo.dataset.large = thumb.dataset.large;
  count.textContent = `写真${thumbs.length}枚中${i + 1}枚目：${photo.alt}`;
  thumbs.forEach((other) =>
    other === thumb ? other.setAttribute('aria-current', 'true') : other.removeAttribute('aria-current'));
}));
// マウスの動きに合わせた拡大は、近道として残す。
frame.addEventListener('mousemove', (event) => {
  const box = frame.getBoundingClientRect();
  const x = ((event.clientX - box.left) / box.width) * 100;
  const y = ((event.clientY - box.top) / box.height) * 100;
  photo.style.transformOrigin = `${x}% ${y}%`;
  photo.style.transform = 'scale(3)';
});
frame.addEventListener('mouseleave', () => { photo.style.transform = ''; });
// ボタンで大きな画像ファイルを開く。フォーカスとEscapeキーはダイアログが処理する。
document.querySelector('.enlarge').addEventListener('click', () => {
  large.src = photo.dataset.large;
  large.alt = photo.alt;
  dialog.showModal();
});
```

1. サムネイルを名前のあるボタンにし、表示中のものに `aria-current` を付けて、枠線をその属性から描きます。ほかの修正と同じです。
2. 必須ではありませんが、どの写真を表示中かを控えめなライブリージョンで伝えます。
3. 「写真を拡大」ボタンを足し、`showModal()` で `dialog` を開きます。フォーカスが中へ移り、背後のページは操作できなくなり、Escapeキーで閉じ、閉じるとフォーカスはボタンへ戻ります。
4. 大きな写真を、Tabの移動先になる枠に入れます。矢印キーで写真の中を動けます。
5. マウスの動きに合わせた拡大は近道として残します。

これで修正前の2つの問題がどちらも解消し、2.1.1と4.1.2を満たします。HTML標準の要素では自分で作ることになる機能のために、ライトボックスのライブラリーを使うこともできます。その場合も、ダイアログをキーボードで使える状態に保ってください。

- 写真のスワイプでの切り替え：前後の写真へのボタンを残し、スワイプだけにしない。
- ピンチでの拡大：拡大のボタンと、キーで写真の中を動く手段を残す。
- 開くときのアニメーション：フォーカスをダイアログへ移し、閉じたらボタンへ戻す動きを変えない。

HTML標準の `dialog` もアニメーションでき、中には上のボタンを含めて自由にマークアップを入れられます。

## 修正後：元サイズの写真へのリンク

```html
<div class="gallery">
  <div class="photo-frame">
    <img class="photo" src="mug-front.jpg" alt="マグの正面">
  </div>
  <p class="photo-count" aria-live="polite">写真3枚中1枚目：マグの正面</p>
  <div class="thumbs">
    <button type="button" class="thumb" aria-current="true" data-photo="mug-front.jpg" data-large="mug-front-large.jpg">
      <img src="mug-front-small.jpg" alt="マグの正面">
    </button>
    <button type="button" class="thumb" data-photo="mug-side.jpg" data-large="mug-side-large.jpg">
      <img src="mug-side-small.jpg" alt="マグの側面">
    </button>
    <button type="button" class="thumb" data-photo="mug-back.jpg" data-large="mug-back-large.jpg">
      <img src="mug-back-small.jpg" alt="マグの背面">
    </button>
  </div>
  <a class="full-size" href="mug-front-large.jpg">元サイズの写真を開く</a>
</div>
```

```css
.photo-frame { aspect-ratio: 1; overflow: hidden; }
.photo { display: block; width: 100%; }
.thumb { border: 2px solid transparent; cursor: pointer; }
/* 表示中のサムネイルは、スクリーンリーダーが読む属性から描く。 */
.thumb[aria-current='true'] { border-color: #2d3a55; }
/* コントラスト テーマではすべての枠線が同じシステム色になるため、表示中のものだけ別の色にする */
@media (forced-colors: active) {
  .thumb[aria-current='true'] { border-color: Highlight; }
}
```

```js
const frame = document.querySelector('.photo-frame');
const photo = frame.querySelector('.photo');
const thumbs = [...document.querySelectorAll('.thumb')];
const count = document.querySelector('.photo-count');
const link = document.querySelector('.full-size');
thumbs.forEach((thumb, i) => thumb.addEventListener('click', () => {
  photo.src = thumb.dataset.photo;
  photo.alt = thumb.querySelector('img').alt;
  link.href = thumb.dataset.large;
  count.textContent = `写真${thumbs.length}枚中${i + 1}枚目：${photo.alt}`;
  thumbs.forEach((other) =>
    other === thumb ? other.setAttribute('aria-current', 'true') : other.removeAttribute('aria-current'));
}));
// マウスの動きに合わせた拡大は、近道として残す。
frame.addEventListener('mousemove', (event) => {
  const box = frame.getBoundingClientRect();
  const x = ((event.clientX - box.left) / box.width) * 100;
  const y = ((event.clientY - box.top) / box.height) * 100;
  photo.style.transformOrigin = `${x}% ${y}%`;
  photo.style.transform = 'scale(3)';
});
frame.addEventListener('mouseleave', () => { photo.style.transform = ''; });
```

1. サムネイルを名前のあるボタンにし、表示中のものに `aria-current` を付けて、枠線をその属性から描きます。ほかの修正と同じです。
2. 必須ではありませんが、どの写真を表示中かを控えめなライブリージョンで伝えます。
3. 元サイズの画像ファイルへのリンクを足し、サムネイルを押すたびに、リンク先を表示中の写真に合わせます。Tabで届き、Enterキーでブラウザーがファイルを開き、「戻る」でページに戻れます。ChromiumとFirefoxは大きな画像をまずウィンドウに収めて表示するので、ブラウザーの拡大表示のキーで大きくします。ウィンドウより大きくなれば、矢印キーで写真の中を動けます。
4. マウスの動きに合わせた拡大は近道として残します。

これで修正前の2つの問題がどちらも解消し、ファイルが細部を見られるだけの大きさであれば、2.1.1と4.1.2を満たします。ここで挙げた中で最も小さな変更で、スクリプトの読み込み前でもリンクは動きます。

## どの方法を選ぶか

どの方法でも基準を満たし、マウスを使う人のための拡大も残せます。違うのは、もっと見たいと思ったときに何が表示されるかです。拡大ボタンはその場で写真を大きくするもので、デザインで占める場所が最も少なくて済みますが、見える範囲は写真の枠の大きさのままで、スマートフォンでは狭くなります。ダイアログは写真に画面の大部分を使え、商品ページでよく見る形です。WooCommerceは、テーマがギャラリーの拡大とライトボックスを有効にしていれば、マウスでの拡大を残したまま、写真を開くコントロールを足します。ShopifyのDawnテーマも、初期設定ではボタンから写真を開きます。マークアップは少し増え、閉じるまでほかの部分は操作できません。リンクは数行で済み、壊れる心配もほとんどありませんが、ページから離れることになります。手間をかけられないとき、あるいはスクリプトが動かなくても大きな写真に届くよう、ダイアログの土台として選んでください。

## 実装の判断

ここで使う状態は、カルーセルの表示中のドットと同じ `aria-current="true"` です。一連の項目のうち、現在の1つを示します。`aria-pressed` では各サムネイルがトグルボタンになり、もう一度押すとオフになるように受け取られます。`aria-selected` はボタンには使えません。ShopifyのDawnテーマとSplideのナビゲーションモードも、ギャラリーのサムネイルに `aria-current` を使っています。

サムネイルに名前を付ける方法はほかにもあり、画像のaltを空にして `aria-label="写真3：マグの背面"` と書くこともできます。altならより少ない記述で同じことができ、ライブリージョンで読むテキストとしてもそのまま使えます。

画像そのものをクリックすると大きく開くメインの写真も、サムネイルと同じ理由で2.1.1を満たしません。ダイアログの修正のように横にボタンを置くか、写真を大きなファイルへのリンクで囲みます。リンクで囲むのはPhotoSwipeがマークアップとして求める形で、Fancyboxの例も同じ形です。スクリプトが読み込まれていればライトボックスが開き、読み込まれていなければファイルが開きます。写真の上のレンズはフォーカスを必要としないので、残してかまいません。

拡大の枠がTabの移動先になる必要があるのは、スクロールできる間だけです。そのため修正では、拡大したときに `tabindex` を付け、戻したら外します。何もしないのに止まる場所があると、キーボード利用者が通り過ぎる手間が増えます。

写真が多いギャラリーでは、サムネイルの列がスクロールし、専用の矢印が付くことがあります。その矢印も名前のあるボタンにし、見えていないサムネイルにフォーカスが移ったら、見える位置までスクロールするようにしてください。

## 修正の確認

1. マウスを使わずに、Tabでギャラリーに入る。サムネイルはすべてTabの移動先になり、EnterキーとSpaceキーでその写真を表示できる。
2. マウスを使わないまま、修正で加えた操作で最後の写真の最も細かい部分まで読み、矢印キーで写真の中を動いて、ページに戻れる。
3. スクリーンリーダーで、Tabでサムネイルを移動する。それぞれどの写真かを伝え、表示中のものはそのことを伝える。別のものを押すと、状態が移り、新しい写真の名前が読まれる。
4. 強制配色（Windowsのコントラスト テーマ）をオンにする。サムネイルと、どれが表示中かが見える。

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

## 制限

このサンプルは、どのアプリケーションでも同じ結果になることを保証するものではありません。扱うのは、サムネイルと拡大に届くか、サムネイルが何を伝えるかです。矢印とドットで切り替えるスライド形式のギャラリーは、[キーボードやスクリーンリーダーで使えないカルーセル](/ja/learn/carousels)で扱います。ライトボックスのフォーカスと閉じ方については、[背後にフォーカスが残るモーダル](/ja/learn/modal-focus-management)を参照してください。ドラッグ、ピンチ、傾きで回したり拡大したりする写真の表示は、[ボタンで代われないピンチ・ドラッグ・傾きのジェスチャー](/ja/learn/gestures)で扱います。各写真のaltに何を書くかは、写真によって変わります。このガイドで求めるのは、サムネイルのaltで写真を区別できることと、写真が変わったら大きな写真のaltも合わせることだけです。

## チェックツールに出るメッセージ

- axe DevTools: "ARIAコマンドにはアクセシブルな名前がなければなりません" (ARIA commands must have an accessible name)
- axe DevTools: "ボタンには認識可能なテキストが存在しなければなりません" (Buttons must have discernible text)
- Lighthouse: "`button`、`link`、`menuitem` 要素にユーザー補助機能向けの名前が設定されていません。" (`button`, `link`, and `menuitem` elements do not have accessible names.)
- Lighthouse: "ボタンにユーザー補助機能名が指定されていません" (Buttons do not have an accessible name)
- WAVE: "Empty button"

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

`aria-current="page"`: メニューやパンくずリスト、ページ送りで、今いるページへのリンクだと示します。

| スクリーンリーダー | ブラウザーと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）
