# チェックマークが読み上げられず空欄と同じになってしまう比較表

Source: https://easeweb.dev/learn/comparison-table-ticks
Topics: 画像とメディア > SVGアイコンが意味を伝えるか、飾りなら読み飛ばされる (WCAG 1.1.1)
The fix: すべてのセルに値を言葉で入れます。視覚的に隠したテキストを足すか、印の隣に言葉を書くか、各印に role="img" と aria-label を付けます。チェックにだけ名前を付けて残りのセルを空にしても1.1.1は満たしますが、「なし」は言葉にならず、空欄で伝えるだけになります。
Test with: screen reader, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA6 https://lucide.dev/guide/react/advanced/accessibility https://docs.fontawesome.com/web/dig-deeper/accessibility https://getbootstrap.com/docs/5.3/examples/pricing/ https://www.w3.org/2007/Talks/0706-atmedia/slides/Slide0520.html https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/img_role

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

## 場面

料金ページで、3つのプランを表で比べています。上にプラン名、左に機能が並び、各セルには小さなアイコンが1つあります。プランに含まれる機能には緑のチェック、含まれない機能にはグレーの×印です。見える人は「シングルサインオン」の行に目を走らせるだけで、Businessにしかないと分かります。スクリーンリーダーの利用者も同じ行をたどります。見出しのマークアップは正しいので、セルごとにプランと機能は分かります。「Starter、シングルサインオン」「Team、シングルサインオン」「Business、シングルサインオン」です。ところが、肝心の値がどのセルにもありません。読み上げる内容のないセルで何と言うかはスクリーンリーダーによりますが、3つとも同じになります。そのため、この表で一番知りたいはずの「どのプランにその機能があるのか」が分かりません。よくあるもう1つの作り方では、チェックだけを描き、「なし」は空のセルにします。チェックが隠れていると、「あり」のセルと「なし」のセルの区別が付きません。

## 確認できる問題

アイコンはアイコンライブラリーから読み込んでいますが、ライブラリーによっては、指定しない限りアイコンを隠します。LucideのコンポーネントとHeroiconsのReactコンポーネントは `aria-hidden="true"` を出力し、Font AwesomeのKitやSVG + JSのSVGアイコンも同じです。同じ内容の言葉が隣にあるアイコンなら、それで問題ありません。ところがここのテンプレートには `<Check />` としか書かれていないので、属性はソースを見ても分からず、セルに言葉もありません。Chromiumのアクセシビリティツリーでは、このセルには名前も子要素もなく、`<td></td>` とまったく同じです。

この表のチェックは飾りではなく、セルの値そのものです。1.1.1 非テキストコンテンツは、同じ目的を果たす代替テキストを求めており、支援技術に無視させてよいのは、純粋な装飾、見た目の整形のためだけのもの、利用者に提示されないものに限られます。隠されたチェックと×印は情報を伝えているのに、代替テキストがありません。空のセルは事情が違います。非テキストコンテンツがないので1.1.1の対象にならず、チェックにだけ名前を付ける方法でも満たせるのはそのためです。

属性がなくても、同じことが起こります。古いバージョンのLucideは `aria-hidden` のないSVGを出力し、Chromiumではこれが名前のない画像になります。やはり読み上げる内容はありません。

## 利用者への影響

どこかが壊れているようには聞こえないので、気づきにくい問題です。プラン名と機能名は順番どおりに聞こえ、答えだけが抜けています。必要な機能があるプランを選びたい人は、サイトの別の場所で探すか、問い合わせるしかありません。

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

axeの `svg-img-alt` と `role-img-alt` が見るのは、画像のロールを持つ要素です。このようなセルの表をChromium上のaxe 4.13で調べると、`role="img"` で名前のないSVGは報告されましたが、同じSVGに `aria-hidden="true"` を付けたもの、属性のないもの、空のセルは報告されませんでした。飾りのアイコンなら隠すのが正しく、このアイコンが飾りでないことはチェックツールには分かりません。この監査では、名前のないSVGだけを持つセルが2つ以上ある表を、人が確認する項目（`ew:table-icon-cells-manual-check`）として挙げます。読み上げる内容がないことはコードで分かりますが、印に意味があるかどうかは人が判断するしかないからです。コードを読む人も、レビューを頼まれたAIアシスタントも、見るのは属性のない `<Check />` です。`aria-hidden` は描画後のページにしか現れません。分かるのは、スクリーンリーダーで行をたどったときか、描画されたセルをアクセシビリティツリーで見たときです。

## 修正前：アイコンライブラリーに隠されたチェックと×印

```html
<table class="plans">
  <thead>
    <tr>
      <th scope="col">機能</th>
      <th scope="col">Starter</th>
      <th scope="col">Team</th>
      <th scope="col">Business</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">共有フォルダー</th>
      <td>
        <svg class="no" aria-hidden="true" viewBox="0 0 24 24">…</svg>
      </td>
      <td>
        <svg class="yes" aria-hidden="true" viewBox="0 0 24 24">…</svg>
      </td>
      <td>
        <svg class="yes" aria-hidden="true" viewBox="0 0 24 24">…</svg>
      </td>
    </tr>
    …
  </tbody>
</table>
```

```css
.plans svg { width: 20px; height: 20px; }
```

見出しは正しく、各セルにプランと機能が対応しています。問題は各セルの中身です。

- チェックは支援技術から隠され、セルにはほかに何もないため、「あり」が伝わりません。
- ×印も隠されているため「なし」も伝わらず、2つのセルが同じになります。

## 修正前：隠されたチェックと空のセル

```html
<table class="plans">
  <thead>
    <tr>
      <th scope="col">機能</th>
      <th scope="col">Starter</th>
      <th scope="col">Team</th>
      <th scope="col">Business</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">共有フォルダー</th>
      <td></td>
      <td>
        <svg class="yes" aria-hidden="true" viewBox="0 0 24 24">…</svg>
      </td>
      <td>
        <svg class="yes" aria-hidden="true" viewBox="0 0 24 24">…</svg>
      </td>
    </tr>
    …
  </tbody>
</table>
```

```css
.plans svg { width: 20px; height: 20px; }
```

違いは×印がないことだけです。画面上では行の内容は明らかですが、次の問題があります。

- チェックが隠されているため、「あり」のセルに読み上げる内容がありません。
- 「なし」のセルは本当に空で、「あり」のセルと同じものになっています。

## 修正後：各セルに隠したテキストを入れる

```html
<table class="plans">
  <thead>
    <tr>
      <th scope="col">機能</th>
      <th scope="col">Starter</th>
      <th scope="col">Team</th>
      <th scope="col">Business</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">共有フォルダー</th>
      <td>
        <svg class="no" aria-hidden="true" viewBox="0 0 24 24">…</svg>
        <span class="visually-hidden">なし</span>
      </td>
      <td>
        <svg class="yes" aria-hidden="true" viewBox="0 0 24 24">…</svg>
        <span class="visually-hidden">あり</span>
      </td>
      <td>
        <svg class="yes" aria-hidden="true" viewBox="0 0 24 24">…</svg>
        <span class="visually-hidden">あり</span>
      </td>
    </tr>
    …
  </tbody>
</table>
```

```css
.plans svg { width: 20px; height: 20px; }
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
```

1. アイコンはライブラリーの出力どおり、隠したままにします。値は言葉で伝わるので、アイコンは読まれなくてかまいません。
2. 各セルに、値を言葉で書いた「あり」か「なし」の `span` を足し、CSSで見えなくします。この言葉がセルのテキストになり、スクリーンリーダーで表を読むときにも、ブラウザーでページを翻訳するときにも使われます。
3. 空のセルがある表では、空のセルにも同じように「なし」を入れます。画面上は空のままで、「なし」と伝わるようになります。

どちらの修正前も直り、1.1.1を満たします。表の見た目は変わりません。画面上の値は印か空欄のままなので、チェックを「あり」と受け取らない人は、推測するしかありません。

## 修正後：印の隣に見える言葉を書く

```html
<table class="plans">
  <thead>
    <tr>
      <th scope="col">機能</th>
      <th scope="col">Starter</th>
      <th scope="col">Team</th>
      <th scope="col">Business</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">共有フォルダー</th>
      <td>
        <svg class="no" aria-hidden="true" viewBox="0 0 24 24">…</svg>
        なし
      </td>
      <td>
        <svg class="yes" aria-hidden="true" viewBox="0 0 24 24">…</svg>
        あり
      </td>
      <td>
        <svg class="yes" aria-hidden="true" viewBox="0 0 24 24">…</svg>
        あり
      </td>
    </tr>
    …
  </tbody>
</table>
```

```css
.plans svg { width: 20px; height: 20px; }
```

1. 各セルの印の隣に、値を「あり」か「なし」と書きます。チェックをどう受け取る人にも、同じ言葉が届きます。
2. アイコンは隠したままにします。隣の言葉を繰り返すだけになったので、隠すのが正しく、言葉は一度だけ読まれます。
3. 空のセルがある表では、そこに「なし」と書きます。この場合、印はなくてもかまいません。

どちらの修正前も直り、1.1.1を満たします。その代わり、場所を取ります。言葉が3列並ぶとアイコンの3列より幅が広がり、長い表では印ならではの見渡しやすさが少し失われます。

## 修正後：各印に名前を付ける

```html
<table class="plans">
  <thead>
    <tr>
      <th scope="col">機能</th>
      <th scope="col">Starter</th>
      <th scope="col">Team</th>
      <th scope="col">Business</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">共有フォルダー</th>
      <td>
        <svg class="no" role="img" aria-label="なし" viewBox="0 0 24 24">…</svg>
      </td>
      <td>
        <svg class="yes" role="img" aria-label="あり" viewBox="0 0 24 24">…</svg>
      </td>
      <td>
        <svg class="yes" role="img" aria-label="あり" viewBox="0 0 24 24">…</svg>
      </td>
    </tr>
    …
  </tbody>
</table>
```

```css
.plans svg { width: 20px; height: 20px; }
```

1. 各印から `aria-hidden` を外し、1つの画像として扱われるように `role="img"` を付けます。
2. それぞれに、値を表す `aria-label` を付けます。「チェック」や「バツ」ではなく、「あり」「なし」です。
3. 空のセルがある表では、先に×印やダッシュなどの印を描きます。空のセルには名前を付ける対象がありません。

すべての「なし」のセルに印がある限り、どちらの修正前も直り、1.1.1を満たします。描画後のページで、ライブラリーがアイコンに `aria-hidden` を残していないか確認してください（「ライブラリーのアイコンに名前を付ける」を参照）。

## 修正後：チェックにだけ名前を付け、残りのセルは空にする

```html
<table class="plans">
  <thead>
    <tr>
      <th scope="col">機能</th>
      <th scope="col">Starter</th>
      <th scope="col">Team</th>
      <th scope="col">Business</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">共有フォルダー</th>
      <td></td>
      <td>
        <svg class="yes" role="img" aria-label="あり" viewBox="0 0 24 24">…</svg>
      </td>
      <td>
        <svg class="yes" role="img" aria-label="あり" viewBox="0 0 24 24">…</svg>
      </td>
    </tr>
    …
  </tbody>
</table>
```

```css
.plans svg { width: 20px; height: 20px; }
```

1. 各チェックから `aria-hidden` を外し、Bootstrapの料金ページのサンプルと同じように、`role="img"` と `aria-label="あり"` で名前を付けます。
2. 「なし」のセルは空のままにし、×印があれば取り除きます。隠した×印は、代替テキストのない印になるからです。

2つ目の修正前に対する、一番小さな変更です。すべてのチェックに代替テキストがあり、空のセルには非テキストコンテンツがないので、1.1.1は満たします。ただし、これだけでは足りません。「なし」は何も読まれないことから判断するしかなく、入力し忘れたセルとの区別も付きません。行をたどる人は、どこかで「あり」を聞いて初めて、空のセルの意味が分かります。

## どの方法を選ぶか

最初の3つは、どれもすべてのセルに値を入れるので、1.1.1を満たし、「なし」も「あり」と同じようにはっきり伝わります。隠したテキストと印の名前は、どちらも今の見た目を保ちます。隠したテキストは普通のテキストなのでARIAが要らず、テキストを扱うものなら何にでも届きます。印に名前を付ける方法では、印は画像として扱われます。`aria-label` を訳さないページ翻訳ツールもあり、印のないセルには使えません。そのため、見た目を保つなら隠したテキストを使います。見える言葉は行に場所を取りますが、表を見られる人にも役立つので、場所があるときや、チェックを「あり」と受け取らない人がいそうなときに選びます。チェックにだけ名前を付ける方法は、すでにそう作られた表の第一歩としては十分ですが、「なし」が言葉になっていないので、できるときに言葉を足してください。

## 実装の判断

**セルのコンポーネントを1つにする。** 値を受け取り、印と言葉を一緒に書き出す1つのコンポーネントからセルを描きます。そうすれば片方だけのセルができず、チェックに違う言葉が付くこともありません。言葉はページのほかの部分と一緒に翻訳します。

**ライブラリーのアイコンに名前を付ける。** ライブラリーによって動きが違うので、テンプレートではなく描画後のページで確認します。Lucideは `aria-label`、`role`、`title` のいずれかを渡すと `aria-hidden` を外します。HeroiconsのReactコンポーネントは、`title` を渡しても、そのプロパティを上書きしない限り `aria-hidden="true"` を残すため、ラベルを渡すだけではアイコンは隠れたままです。

**「あり」「なし」以外の値があるセル。** 「5人まで」「オプション」のような値はもともとテキストなので、何も要りません。注記の付いた印は、注記の内容も言葉に入れます。「あり、年払いのみ」のようにします。

**機能リストの印も同じ。** 料金カードでは、含まれる機能にチェック、含まれない機能に×印や取り消し線を付けて並べることがよくあり、スマートフォンではこの形のカードに変わる表もあります。印が隠れていると、空欄より悪い結果になります。リストは普通のテキストとして読まれ、すべての機能が使えるように聞こえるからです。直し方は同じで、含まれない機能の項目それぞれに、言葉で「なし」と入れます。

**チェックがどこでも「あり」を意味するとは限らない。** W3Cの講演資料には、日本などではチェックマークが「誤り」を示すことがあり、日本向けのローカライズでは ○ に置き換えることが多いと書かれています。見える言葉なら、どこで売っていても迷いません。SVGではなく文字のチェックや×については、[意味ではなく名前で読み上げられる絵文字](/ja/learn/emoji)で扱っています。

**コントラスト。** 値を伝える印には、1.4.11により背景との3:1のコントラストが必要です。薄いグレーの×印は足りないことがあります。[コントラストが足りない入力欄・アイコン・フォーカス枠](/ja/learn/non-text-contrast)を参照してください。

## 修正の確認

1. ブラウザーのアクセシビリティツリーで、描画後のページで、状態ごとにセルを開きます。「あり」か「なし」が名前かテキストになっているはずです。チェックにだけ名前を付けた場合、「なし」のセルは空で、それがこの方法の限界です。
2. スクリーンリーダーで行をたどります。各プランの値が言葉で読まれ、機能があるプランとないプランが同じに聞こえないはずです。
3. 表を見られる人に、行ごとに印と言葉を比べてもらいます。チェックに「なし」、×印に「あり」が付いていないはずです。
4. ページをスマートフォンの幅まで狭めます。表がカードやリストに変わるなら、もう一度読みます。同じ値が言葉で書かれていて、含まれない機能まで使えるように聞こえないはずです。

OS、ブラウザー、支援技術のバージョン、ビルド、日付、各手順の実際の結果を記録してください。アイコンライブラリーやそのバージョン、共有しているセルのコンポーネントが変わるたびに繰り返します。

## 制限

このサンプルは、どの表でも同じ結果になることを保証するものではありません。空のセルで何と言うか、セルの中の画像をどう読むかはスクリーンリーダーによって違うので、利用者が使う支援技術で確認してください。セルの言葉が役に立つのは、セルに見出しもあるときだけです。「あり」と読まれても、プランと機能が分からなければ意味がありません。`div` で描いた比較表や、見出しを太字にしただけの比較表はそこで問題になります。[見出しが太字だけのデータ表](/ja/learn/table-headers)を参照してください。表の外のアイコンに名前を付けることと飾りのアイコンを隠すことは[名前がない、または読まれてしまうSVGアイコン](/ja/learn/svg-icons)、ボタンの中のアイコンは[名前がないアイコンだけのボタン](/ja/learn/icon-buttons)で扱っています。

---

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