# リンクと読み上げられ、Spaceキーで押せないボタン

Source: https://easeweb.dev/learn/links-as-buttons
Topics: ボタンとリンク > ページ移動にはリンク、操作にはボタンを使う (WCAG 4.1.2)
The fix: 丸いボタンの見た目はそのままに、type="button"のbutton要素で作ります。リンクを残すしかない場合は、role="button"を付け、Spaceキーで押せるキー操作の処理を加えます。role="button"だけでは不十分で、Spaceを押すと今までどおりページがスクロールします。
Test with: keyboard, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/ARIA/apg/patterns/button/ https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/button_role

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

## 場面

あるお店の商品ページに、売り切れになったリネンのトートバッグが表示されています。価格の下には、深緑の丸いボタン「再入荷のお知らせ」があります。押すと、メールアドレスを入力する小さなフォームがページの上に開きます。見た目はボタンで、働きもボタンです。その場で何かを開くだけで、どこへも移動しません。

ところが、作りはリンクです。キーボード利用者がTabで丸いボタンへ移動し、ボタンを押すつもりでSpaceを押すと、ページが1画面分下へスクロールします。丸いボタンは画面の外へ流れ、フォームは開きません。Enterなら開きます。スクリーンリーダーの利用者には「再入荷のお知らせ、リンク」と聞こえます。リンクは別のページへ移動するものなので、この商品ページから離れると思い込みます。入荷の連絡を受け取る方法をページのボタン一覧から探しても、見つかりません。この操作要素は、リンク一覧のほうに入っているからです。

## 確認できる問題

達成基準4.1.2「名前（name）・役割（role）及び値（value）」は、支援技術が操作要素の種類を伝えられるよう、すべての操作要素の役割（ロール）をコードから判別できることを求めています。`href` を持つ `a` 要素は、ブラウザーにとってリンクであり、スクリーンリーダーにもリンクというロールを渡します。フォームを開くクリック処理を加えても、丸いボタンの見た目にするクラスを付けても、ロールは変わりません。そのため、コード上のロールはリンクのままで、働きだけがボタンです。ARIA Authoring Practicesも、ボタンが行う操作はリンクの機能とはっきり異なり、ウィジェットの見た目とロールはどちらも、その機能に合わせるべきだと述べています。

キー操作もロールに従います。ブラウザーはリンク上のEnterをクリックとして扱いますが、リンク上のSpaceはページをスクロールするキーです。そのため、Spaceではクリック処理が実行されません。HTML標準のボタンなら、EnterでもSpaceでもクリック処理が実行されます。`href="#"` は、リンクにフォーカスが当たるようにし、ページの再読み込みを防ぐためだけに付けられていますが、この値にも別の落とし穴があります。「リンクを新しいタブで開く」を選ぶと同じページの複製が開き、スクリプトの読み込みが遅れていると、押したときにページの先頭へ飛びます。

## 利用者への影響

マウスでもEnterでも動くため、簡単な確認では問題が見つかりません。問題が表に出るのは、操作要素が何と読み上げられるかを頼りにする人と、ボタンをSpaceで押す人だけです。

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

axeのようなルールベースの自動チェックには、名前とアドレスのあるリンクとしか見えないため、何も報告されません。フォームを開くリンクも、リンクとしては正しいからです。その操作要素が何をするかは、実際に押すまで分かりません。EaseWebの診断は、どこも指していないリンク（`href="#"`、`"#!"`、`""`、`javascript:` URL）を、人が押して確かめる対象として一覧にします。ただし、実在するアドレスを持ちながら、スクリプトでフォームを開くリンクは、問題がないように見えます。コード生成AIが手軽な修正として提案することのある `role="button"` の追加は、読み上げを変えるだけで、Spaceによるスクロールは残ります。

## 修正前：丸いボタンに見せた、どこも指していないリンク

```html
<p class="stock">売り切れ</p>
<a href="#" class="pill notify">
  再入荷のお知らせ
</a>
<dialog id="notify" aria-labelledby="notify-title">
  …
</dialog>
```

```css
.pill {
  display: inline-block;
  padding: 10px 20px;
  border-radius: 999px;
  background: #1f4d3f;
  color: #fff;
  font-weight: bold;
  text-decoration: none;
}
```

```js
const notify = document.querySelector('.notify');
notify.addEventListener('click', (event) => {
  event.preventDefault();
  document.getElementById('notify').showModal();
});
```

丸いボタンは `a` 要素なのでリンクになり、働きだけをスクリプトでボタンにしています。スクリーンリーダーはリンクと伝え、ボタンなら押せるはずのSpaceでは、ページがスクロールします。`href="javascript:void(0)"` のリンクや、アドレスは実在してもスクリプトが毎回移動を取り消すリンクも同じです。

## 修正後：button要素を使う

```html
<p class="stock">売り切れ</p>
<button type="button" class="pill notify">
  再入荷のお知らせ
</button>
<dialog id="notify" aria-labelledby="notify-title">
  …
</dialog>
```

```css
.pill {
  display: inline-block;
  padding: 10px 20px;
  border: 0;
  border-radius: 999px;
  background: #1f4d3f;
  color: #fff;
  font: inherit;
  font-weight: bold;
  cursor: pointer;
}
```

```js
const notify = document.querySelector('.notify');
notify.addEventListener('click', () => {
  document.getElementById('notify').showModal();
});
```

button要素はボタンとして読み上げられ、ブラウザーはEnterでもSpaceでもクリック処理を実行します。キー操作の処理は不要で、ページがスクロールすることもありません。`type="button"` を付けると、フォームの中に置いても、そのフォームを送信しません。CSSでブラウザー標準の枠線と灰色の背景を取り、フォントを指定すれば、丸いボタンの見た目は元のままです。

## 修正後：リンクを残す場合のrole="button"とSpace

```html
<p class="stock">売り切れ</p>
<a href="#" role="button" class="pill notify">
  再入荷のお知らせ
</a>
<dialog id="notify" aria-labelledby="notify-title">
  …
</dialog>
```

```css
.pill {
  display: inline-block;
  padding: 10px 20px;
  border-radius: 999px;
  background: #1f4d3f;
  color: #fff;
  font-weight: bold;
  text-decoration: none;
}
```

```js
const notify = document.querySelector('.notify');
notify.addEventListener('click', (event) => {
  event.preventDefault();
  document.getElementById('notify').showModal();
});
// ボタンと同じく、Spaceを離したときに押し、ページはスクロールさせない。
notify.addEventListener('keydown', (event) => {
  if (event.key === ' ') event.preventDefault();
});
notify.addEventListener('keyup', (event) => {
  if (event.key === ' ') notify.click();
});
```

`role="button"` を付けたので、スクリーンリーダーはボタンと読み上げます。2つのキー処理は、HTML標準のボタンと同じくSpaceを離したときに押されるようにし、ページのスクロールも止めます。Enterでは、もともとリンクがクリックされます。テーマやプラグインが出力するリンクのように、属性やスクリプトは加えられても要素自体は変えられない場合の方法です。ブラウザーのメニューでは要素がリンクのまま扱われるため、「リンクを新しいタブで開く」を選んだりドラッグしたりすると、同じページが開きます。

## 修正にならない応急処置

```html
<a href="#" role="button" class="pill notify">
  再入荷のお知らせ
</a>
```

`role="button"` を付けると、スクリーンリーダーは「ボタン」と読み上げるので、ロールの問題は直ります。しかし、ARIAが変えるのは読み上げだけです。ブラウザーにとってはリンクのままで、Spaceを押すとページがスクロールします。スクリーンリーダーの利用者はボタンだと伝えられるのに、どのボタンでも使えるはずのキーが効きません。上の2つ目の修正は、残りを手作業で補ったものです。HTML標準のボタンなら、その手間はいりません。

## どの方法を選ぶか

どちらの方法でも基準を満たします。button要素を使ってください。ロールも両方のキーもブラウザーが受け持つため、コードが少なく、失うものもありません。実在しないアドレスについて説明する必要もありません。`role="button"` とSpaceの処理を加えたリンクは、要素を変えられない場合に限って使い、テーマやプラグインを更新するたびにキー操作を確認し直してください。

## 実装の判断

見た目とロールは揃えます。デザイン上必要なら、ボタンをリンクのような見た目にすることもできます。ただし、下線の付いた言葉はふつうリンクの見た目なので、目で見て操作する人には「どこかへ移動する」ように見えます。そのためAuthoring Practicesは、操作要素の働きに合わせて見た目を調整することを勧めています。丸いボタンやボタンの形なら、その場で何かをする要素だと誰にでも伝わります。

この問題は、共通のコンポーネントから始まることがよくあります。`href` を渡されたときだけ `a` を、それ以外は `button` を出力する `Button` コンポーネントなら、2つを分けられます。いつも `a` を出力するものや、フォーカスを当てるために `href="#"` を渡すものでは、すべての操作がリンクになります。ルーターのリンクコンポーネントを操作に使った場合も同じです。

かごに入れる、購読するなど、サーバー側で何かを変える操作も、操作内容をアドレスに含めたリンクではなく、ボタンにします。リンクは先読み、ブックマーク、共有の対象になり、もう一度開くと同じ操作が繰り返されてしまうからです。送信ボタンのあるフォームで送るか、ボタンのクリック処理からサーバーを呼び出してください。

押した後の動作は別の課題です。`showModal()` で開いたHTML標準の `dialog` は、フォーカスをフォームの中へ移し、閉じるとボタンへ戻します。ほかの方法で作るダイアログについては、[背後にフォーカスが残るモーダル](/ja/learn/modal-focus-management)を参照してください。

## 修正の確認

1. スクリーンリーダーを使い、Tabで操作要素へ移動して、表示どおりの言葉と「ボタン」が読み上げられることを確認する。
2. Enterを押し、開いたフォームを閉じてから、Spaceを押す。どちらでもフォームが1回開き、ページがスクロールしないことを確認する。
3. スクリーンリーダーのボタン一覧を開き、操作要素がそこにあり、リンク一覧にはないことを確認する。
4. ページ上で、何かを開く、変える、送るほかの操作要素もすべて押し、それぞれ手順1から3を繰り返す。

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

## 制限

このガイドが扱うのは、ボタンの働きをするリンクです。逆に、別のページへ移動するだけのボタンは、ボタンとして読み上げられ、新しいタブで開くこともリンク一覧で見つけることもできません。同じトピックを反対側から見た問題で、今後のガイドで扱う予定です。ロールのないdivやspanにクリック処理を設定した場合は、[Tabキーで到達できないdivのボタン](/ja/learn/native-buttons)を参照してください。ページをその場で絞り込んだり並べ替えたりするリンクは、ページを更新するリンクのトピックで扱います。言葉のないボタンについては、[名前がないアイコンだけのボタン](/ja/learn/icon-buttons)を参照してください。対象とする支援技術で、操作全体を確認してください。

---

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