# カードのリンクの中にあり、押すとリンク先も開いてしまうボタン

Source: https://easeweb.dev/learn/card-link-buttons
Topics: ボタンとリンク > ボタンやリンクの中に、別のボタンやリンクを入れていない (WCAG 4.1.2)
The fix: お気に入りボタンをリンクの外へ出し、CSSで写真の上に配置します。または、商品名だけをリンクにし、擬似要素でその範囲をカード全体に広げ、ボタンをその上に重ねます。クリックがリンクへ伝わらないようにするだけでは修正になりません。ボタンはリンクの中に残ったままです。
Test with: keyboard, screen reader, mouse, touch
References: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/WCAG22/Understanding/parsing.html https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element https://inclusive-components.design/cards/ https://dequeuniversity.com/rules/axe/4.13/nested-interactive

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

## 場面

あるお店が、マグカップをカードの一覧で紹介しています。カードには写真、商品名、価格があります。カードのどこを押しても商品ページが開くと押しやすいので、開発者は各カードを1つのリンクで囲みました。写真の隅にある「お気に入りに追加」のハートも、ほかの要素と一緒にリンクの中に入っています。

買い物客がSpeckled mugのハートを押します。ハートは塗りつぶされ、続いて商品ページが開きます。ハートを押すことは、リンクの中を押すことでもあるため、ブラウザーはハートの処理が終わるとリンク先へ移動します。Chrome、Firefox、Safariのエンジンのどれでも同じで、クリックでも、タップでも、EnterやSpaceで押しても変わりません。2つ目のマグを保存するには、一覧へ戻り、さっきの場所を探し直すことになります。

スクリーンリーダーの利用者は、カードの中を1つずつ移動します。最初に止まるのはリンクで、Chromeではその名前が「Speckled mug $24 お気に入りに追加」になります。リンクが、お気に入りの操作まで名乗っています。次に止まるのがハートで、同じリンクの中にある別のボタンです。押したときにどちらが動くのかは、読み上げからは分かりません。実際には、ハートを押すと両方が動きます。

## 確認できる問題

達成基準4.1.2「名前（name）・役割（role）及び値（value）」は、支援技術が各コントロールの種類と働きを伝えられるよう、すべてのコントロールの名前と役割（ロール）をコードから判別できることを求めています。この例では、2つのコントロールが絡み合っています。リンクは中にあるものすべてから名前を作るため、ハートの「お気に入りに追加」がリンクの名前に入り、リンクがしない操作まで読み上げられます。ボタンはリンクの中にあると伝えられるので、押したときに何が起きるかはロールから決まらず、3つのブラウザーエンジンのどれでも両方の動作が起きます。

HTMLはこの書き方を認めていません。`a` 要素の中には、ボタン、別のリンク、フォームの入力欄などの対話型コンテンツを入れられず、`button` 要素の中にも入れられません。HTMLパーサーは直せるところを直し、残りはそのままにするため、ページは表示されます。しかし、コントロールの中のコントロールが何を意味するかは、HTMLでは定められていません。今のブラウザーは上で見たとおり同じように動きますが、どう読み上げるかはスクリーンリーダーごとに異なります。WCAG 2.2では、こうしたマークアップを確認していた4.1.1「構文解析」が削除されました。W3Cは削除についての説明で、4.1.1で不適合になっていた問題の多くは、今は1.3.1「情報及び関係性」か4.1.2で不適合になると述べています。リンクの中のボタンもその一つで、理由は上で見た名前とロールへの影響です。

## 利用者への影響

マウスで押すと両方の動作が起きるため、簡単な確認では「動いている」と判断されがちです。問題がはっきり表れるのは、スマートフォンやタッチ画面です。ハートをタップするたびに一覧から移動してしまいます。長い一覧では、戻るたびに同じ場所までスクロールし直すことになります。

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

axeのルール `nested-interactive` は、ボタンのように中身ごと1つのものとして読まれるコントロールの中に、フォーカスを受け取る要素がないかを調べます。ボタンの中のリンクは報告されます。リンクの中のボタンは報告されません。ARIAでは、リンクの中身はそれぞれ別の項目として扱われるためです。そのため、このガイドのカードはaxeでは問題なしになります。EaseWebの診断は、リンクの中で見つかったボタン、リンク、入力欄を、人が押して確かめる対象として一覧にします。ハートを押してもページが開かないようにしてほしいとコード生成AIに頼むと、下の応急処置のように、ハートの処理に `preventDefault()` と `stopPropagation()` を加えることがあります。ページは開かなくなりますが、ボタンはリンクの中に残ります。

## 修正前：カード全体を1つのリンクで囲み、その中にハートがある

```html
<ul class="cards">
  <li class="card">
    <a href="/mugs/speckled" class="card-link">
      <img src="speckled-mug.webp" alt="">
      <h3>Speckled mug</h3>
      <p class="price">$24</p>
      <button type="button" class="wish" aria-label="お気に入りに追加" aria-pressed="false">
        <svg aria-hidden="true">…</svg>
      </button>
    </a>
  </li>
  …
</ul>
```

```css
.card-link {
  position: relative;
  display: grid;
  gap: 8px;
  color: inherit;
  text-decoration: none;
}
.wish {
  position: absolute;
  top: 8px;
  right: 8px;
}
```

- ハートの言葉がリンクの名前に入り、リンクは「Speckled mug $24 お気に入りに追加」と読み上げられます。
- マウス、指、Enter、Spaceのどれでハートを押しても、マグが保存されたうえで商品ページが開きます。
- スクリーンリーダーには、HTMLが認めていない「リンクの中のボタン」が渡されるため、どう読み上げるかはスクリーンリーダーごとに異なります。

## 修正後：ハートをリンクの横に置く

```html
<ul class="cards">
  <li class="card">
    <a href="/mugs/speckled" class="card-link">
      <img src="speckled-mug.webp" alt="">
      <h3>Speckled mug</h3>
      <p class="price">$24</p>
    </a>
    <button type="button" class="wish" aria-label="お気に入りに追加" aria-pressed="false">
      <svg aria-hidden="true">…</svg>
    </button>
  </li>
  …
</ul>
```

```css
.card {
  position: relative;
}
.card-link {
  display: grid;
  gap: 8px;
  color: inherit;
  text-decoration: none;
}
.wish {
  position: absolute;
  top: 8px;
  right: 8px;
}
```

1. ボタンを `a` の外へ出し、カードの中でリンクの後ろに置きます。リンクが囲むのは写真、商品名、価格になり、名前は「Speckled mug $24」になります。
2. `position: relative` をリンクからカードへ移します。ハートは写真の隅に残り、見た目は元のままです。
3. ボタンの処理は変えません。ハートを押して動くのはハートだけになり、一覧はそのまま残ります。

これで修正前の例は直り、4.1.2を満たします。ソース上でハートがリンクの後ろにあるため、Tabはリンク、ハートの順に移動し、順序も前と同じです。

## 修正後：商品名をリンクにして、カード全体に広げる

```html
<ul class="cards">
  <li class="card">
    <img src="speckled-mug.webp" alt="">
    <h3>
      <a href="/mugs/speckled" class="card-link">Speckled mug</a>
    </h3>
    <p class="price">$24</p>
    <button type="button" class="wish" aria-label="お気に入りに追加" aria-pressed="false">
      <svg aria-hidden="true">…</svg>
    </button>
  </li>
  …
</ul>
```

```css
.card {
  position: relative;
  display: grid;
  gap: 8px;
}
.card-link {
  color: inherit;
  text-decoration: none;
}
.card-link::after {
  content: "";
  position: absolute;
  inset: 0;
}
.card-link:focus-visible {
  outline: none;
}
.card-link:focus-visible::after {
  outline: 3px solid #1a4fd0;
  outline-offset: 3px;
}
.wish {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 1;
}
```

1. リンクの中身を、見出しの中の商品名だけにします。リンクの名前は「Speckled mug」になり、写真、価格、ハートはその横に並びます。
2. リンクに、カードを基準に配置した `::after` の箱を付け、カード全体を覆います。カードのどこをクリックやタップしても、リンクの一部であるこの箱に当たるため、カード全体で商品ページが開くのは前と同じです。
3. フォーカスの枠を商品名ではなくこの箱に描きます。キーボード利用者には、前と同じくカード全体を囲む枠が見えます。
4. ハートに `z-index: 1` を付け、リンクの箱より上に重ねます。ハートのクリックは、ハート自身が受け取ります。

これで修正前の例は直り、4.1.2を満たします。リンクの箱はカードの文字も覆うため、商品名や価格をマウスで選択できなくなります。カードにほかのリンクやボタンを加える場合は、それぞれに `z-index` が必要です。

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

```js
wish.addEventListener('click', (event) => {
  event.preventDefault();
  event.stopPropagation();
  …
});
```

`preventDefault()` はブラウザーがリンク先へ移動するのを止め、`stopPropagation()` はクリックがリンク側のルーターの処理へ届くのを止めます。これで、ハートを押してもページは開かなくなります。ほかは何も変わりません。ボタンはリンクの中にあるので、リンクの名前は「Speckled mug $24 お気に入りに追加」のままで、マークアップはHTMLの決まりに反したままです。リンクの中のボタンをどう扱うかも、スクリーンリーダー任せのままです。マウスで操作する人が気づく症状を一つ隠すだけです。

## どの方法を選ぶか

どちらの方法でも基準を満たします。ハートをリンクの外へ出すほうが、変更は小さく済みます。写真、商品名、価格を囲むリンクは1つのまま残り、CSSは `position: relative` を移すだけで、文字も選択できます。ただし、リンクは中身をすべて読み上げます。商品名と価格だけなら問題ありませんが、評価、色の選択肢、説明文まで並ぶカードでは長くなります。そうしたカードや、カードにコントロールが複数ある場合は、商品名のリンクをカード全体に広げる方法を選んでください。リンクの名前は商品名だけになり、各コントロールはその横に並びます。

## 実装の判断

同じ入れ子は、手書きのマークアップと同じくらい、コンポーネントからも生まれます。ルーターのリンクコンポーネントでデザインシステムの `Button` を囲むと、リンクの中にボタンが入ります。1つの操作にTabの停止位置が2つでき、押すと両方が動きます。要素を1つだけ出力してください。多くのライブラリーには、`as`、`component`、`asChild` などのプロパティーでリンクコンポーネントをボタンへ渡す方法があり、1つの要素がリンクのアドレスとボタンの見た目を併せ持てます。ルート要素がリンクのカードコンポーネントや、カードの押せる領域がボタンになっているものも、中に押せるものを入れたとたんに同じ問題が起きます。そうしたコンポーネントでは、操作をカードの押せる領域の外に置いてください。

ハートの `aria-pressed` はマグが保存済みかどうかを伝え、`aria-label` はどちらの状態でも同じ名前を保ちます。スクリーンリーダーはトグルボタンとして読み上げ、押されているかどうかを伝えます。この方法と、ほかの選択肢については[押された状態が伝わらないトグルボタン](/ja/learn/toggle-buttons)を参照してください。

ハートの名前は、どのカードでも「お気に入りに追加」です。スクリーンリーダーの利用者は商品のリンクのすぐ後にハートへ移動するので、位置からどのマグを保存するかは分かります。ただし、ページのボタン一覧では、ハートがすべて同じ名前で並びます。「Speckled mugをお気に入りに追加」のように商品名を加えると、一覧の中でもそれぞれを区別できます。

カード全体で商品ページを開くのは便利さのためで、必須ではありません。商品名だけをリンクにし、カードのほかの部分を押せないままにしても基準を満たし、配置の工夫も要りません。カード全体を押せるようにする場合は、両方の修正と同じように、リンクのフォーカスの枠をカード全体に描き、Enterで何が開くのかをキーボード利用者に見せてください。

## 修正の確認

1. ページのマークアップかブラウザーの検証ツールで、リンクの中にボタン、リンク、入力欄、tabindexを持つ要素がなく、ボタンの中にリンクやフォーカスを受け取る要素がないことを確認する。
2. スクリーンリーダーを使い、Tabでカードのリンクへ移動して、商品の言葉だけが読み上げられることを確認する。もう一度Tabを押し、ハートが別に「お気に入りに追加」という名前のトグルボタンとして読み上げられることを確認する。
3. ハートをマウスで押し、次にEnter、Spaceで押し、タッチ画面ではタップする。そのたびにハートの状態が切り替わり、一覧の表示もアドレスも変わらないことを確認する。
4. 検索結果、関連商品、カートなど、サイトにあるすべての種類のカードと、リンクやボタンの中にコントロールがあるほかのコンポーネントで、手順1から3を繰り返す。

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

## 制限

このガイドが扱うのは、カードのリンクの中に置いたボタンです。逆にボタンの中にリンクを置く形はaxeが報告するもので、直し方は同じく2つを並べて置くことです。言葉のないリンクをバナーやカードに重ねた場合は、名前のないリンクの問題です。[名前のないバナーや画像のリンク](/ja/learn/empty-links)を参照してください。どのカードも同じリンクの言葉で終わる場合は[どれも同じ「続きを読む」のリンク](/ja/learn/read-more)、ホバーでしか現れないカードの操作は[ホバーでしか現れないカードの操作ボタン](/ja/learn/hover-actions)、ボタンの働きをするリンクは[リンクと読み上げられ、Spaceキーで押せないボタン](/ja/learn/links-as-buttons)を参照してください。対象とする支援技術で、操作全体を確認してください。

---

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