# 一部選択の状態が読み上げられない「すべて選択」チェックボックス

Source: https://easeweb.dev/learn/select-all-checkboxes
Topics: フォーム > 「すべて選択」のチェックボックスが一部だけ選択された状態を示す (WCAG 4.1.2)
The fix: 一部チェックの状態を、見た目だけでなくコードに持たせます。HTML標準のチェックボックスなら、スクリプトで indeterminate プロパティを設定して :indeterminate から横線を描きます。「すべて選択」が独自のチェックボックスなら、aria-checked="mixed" を設定してそこから横線を描きます。
Test with: screen reader, keyboard, forced colors
References: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://html.spec.whatwg.org/multipage/input.html#dom-input-indeterminate https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/checkbox#indeterminate_state_checkboxes https://developer.mozilla.org/en-US/docs/Web/CSS/:indeterminate https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/examples/checkbox-mixed/ https://www.w3.org/TR/html-aria/#docconformance

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

## 場面

アカウントのページに、請求書の一覧があります。各行にチェックボックスがあり、一覧の上に「すべて選択」のチェックボックス、下に「選択した請求書をダウンロード」ボタンがあります。3月と4月をチェックすると、「すべて選択」のボックスが空から横線に変わります。この横線は一覧のまとめです。一部の請求書がチェックされていて、すべてではないことが一目で分かります。長い一覧では、画面の外の行がまだチェックされていることを示す、ただ一つの手がかりになることもよくあります。

スクリーンリーダーで同じ2件をチェックし、Shift + Tabで「すべて選択」に戻ります。読み上げは「すべて選択、チェックボックス、チェックなし」です。画面のボックスには横線が出ていますが、コード上のチェックボックスは、ただチェックされていない状態です。Spaceを押すと、マウスのときと同じようにすべての請求書がチェックされるので、操作はできます。問題は伝える内容です。あとから一覧に来た人や、上から順に読む人は、「すべて選択」がチェックなしだと聞き、すべての行を読まない限り、すでに2件が選ばれていることを知る方法がありません。

## 確認できる問題

達成基準4.1.2 名前・役割・値は、利用者が設定できるコントロールの状態を、支援技術が取得できることを求めています。横線の付いたチェックボックスは、一部だけチェックされた第3の状態にあり、その状態は画面上にしかありません。

HTMLのチェックボックスのチェックの値は、チェックありとチェックなしの2つです。3つ目の見た目を決めるのは `indeterminate` という別のフラグで、スクリプトから扱うプロパティとしてしか存在しません。`indeterminate` という属性はないため、マークアップに書くことも、テンプレートで属性として設定することもできません。スクリプトでこのプロパティを `true` にすると、ブラウザーは標準の横線を描き、CSSでは `:indeterminate` に一致するようになり、支援技術にはチェックボックスを一部チェック（mixed）の状態として伝えます。スクリーンリーダーは、一部だけチェックされた状態として読み上げます。言い方はスクリーンリーダーによって異なります。この例のスクリプトは、このプロパティを一度も設定していません。クラスを付け、CSSがそのクラスからボックスに横線を描いています。見た目とブラウザーが伝える状態は別々で、変わったのは見た目だけです。

同じ横線は、ほかの方法でもよく作られ、同じように失敗します。

- HTML標準のチェックボックスに `aria-checked="mixed"` を付ける。ARIA in HTMLは `input type="checkbox"` への `aria-checked` を禁じており、Chromeはこの属性を無視してinput自身のチェックの状態を伝えるため、ボックスはチェックなしと読まれたままです。
- `role="checkbox"` を持つ `button` や `div` で作った独自のチェックボックスで、スクリプトが `aria-checked` を `"true"` か `"false"` にしかせず、`"mixed"` にしない。
- 1行でもチェックされると `checked = true` にし、チェックマークの上に横線を描くスクリプト。ボックスには横線が出ますが、読み上げは「チェックあり」で、すべての請求書が選ばれているかのように伝わります。

## 利用者への影響

「すべて選択」は、請求書、メッセージ、ファイル、注文、表の行など、画面に収まらない一覧の上に置かれます。一覧全体の状態をまとめて示すのは、このチェックボックスの見た目だけです。そのまとめが描かれているだけでは、画面を見ない人にはまったく届きません。

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

チェックボックスにはラベルもロールも正しい状態もあるため、axeなどの自動チェックは合格にします。マークアップに誤りはなく、足りないものがあるだけです。また、自動チェックは見た目と伝わる状態を比べません。EaseWebの診断は、1行をSpaceでチェックし、見た目が変わったのにチェックなしと読まれたままの「すべて選択」を `ew:select-all-state-missing` として報告します。修正を頼むと、コード生成AIはHTML標準のチェックボックスに `aria-checked="mixed"` を付けたり、マークアップに `indeterminate` を属性として書いたりすることがあります。どちらもChromeが伝える内容を変えず、チェックボックスはチェックなしと読まれたままです。前者は、HTML標準のチェックボックスの `aria-checked` が実際の状態と食い違うため、axeが `aria-conditional-attr` として見つけます。ただし、求められる修正は属性を外すことなので、外せば横線はまた伝わらなくなります。クラスではなく、ブラウザーのアクセシビリティパネルで「すべて選択」の状態を確認してください。

## 修正前：クラスから描いた横線

```html
<form action="/invoices/download" method="post">
  <label class="check">
    <input type="checkbox" id="select-all">
    すべて選択
  </label>
  <ul class="invoices">
    <li>
      <label class="check">
        <input type="checkbox" name="invoice" value="2026-03">
        2026年3月
      </label>
    </li>
    …
  </ul>
  <button type="submit">選択した請求書をダウンロード</button>
</form>
```

```css
/* チェックボックス自体に描いたボックスとチェックマーク */
.check input { … }
.check input:checked { … }
/* 一部の行をチェック：チェックマークの代わりに横線 */
.check input.partial {
  border-color: #4f46e5;
  background: #4f46e5;
}
.check input.partial::before {
  visibility: visible;
  inline-size: 0.625rem;
  block-size: 0;
  border-width: 0 0 3px;
  rotate: none;
}
```

```js
const all = document.querySelector('#select-all');
const rows = [...document.querySelectorAll('input[name="invoice"]')];

// 「すべて選択」で、すべての行をチェックするか外す。
all.addEventListener('change', () => {
  rows.forEach((row) => { row.checked = all.checked; });
  all.classList.remove('partial');
});

// 行をチェックすると「すべて選択」を更新する。すべてならチェック、一部なら横線。
rows.forEach((row) => row.addEventListener('change', () => {
  const ticked = rows.filter((r) => r.checked).length;
  all.checked = ticked === rows.length;
  all.classList.toggle('partial', ticked > 0 && !all.checked);
}));
```

2行がチェックされていると、スクリプトは `checked` を `false` にして `partial` クラスを付け、CSSがそのクラスから横線を描きます。ブラウザーが伝えるのは、チェックボックスに描かれたものではなく、チェックボックスが持つ状態なので、スクリーンリーダーは「チェックなし」と読み上げます。

## 修正後：indeterminate プロパティ

```html
<form action="/invoices/download" method="post">
  <label class="check">
    <input type="checkbox" id="select-all">
    すべて選択
  </label>
  <ul class="invoices">
    <li>
      <label class="check">
        <input type="checkbox" name="invoice" value="2026-03">
        2026年3月
      </label>
    </li>
    …
  </ul>
  <button type="submit">選択した請求書をダウンロード</button>
</form>
```

```css
/* チェックボックス自体に描いたボックスとチェックマーク */
.check input { … }
.check input:checked { … }
/* 一部の行をチェック：チェックマークの代わりに横線 */
.check input:indeterminate {
  border-color: #4f46e5;
  background: #4f46e5;
}
.check input:indeterminate::before {
  visibility: visible;
  inline-size: 0.625rem;
  block-size: 0;
  border-width: 0 0 3px;
  rotate: none;
}
```

```js
const all = document.querySelector('#select-all');
const rows = [...document.querySelectorAll('input[name="invoice"]')];

// 「すべて選択」で、すべての行をチェックするか外す。
all.addEventListener('change', () => {
  rows.forEach((row) => { row.checked = all.checked; });
});

// 行をチェックすると「すべて選択」を更新する。すべてならチェック、一部なら一部チェック。
rows.forEach((row) => row.addEventListener('change', () => {
  const ticked = rows.filter((r) => r.checked).length;
  all.checked = ticked === rows.length;
  all.indeterminate = ticked > 0 && !all.checked;
}));
```

スクリプトはクラスを付ける代わりに、チェックボックス自身の `indeterminate` プロパティを設定し、CSSはプロパティが `true` の間だけ一致する `:indeterminate` から横線を描きます。スクリーンリーダーが読む状態と目に見える横線が一つの値から決まるので、食い違うことがありません。`remove('partial')` の行もなくなります。一部チェックのチェックボックスを押すと、ブラウザー自身が `indeterminate` を `false` に戻してチェックを付けるため、「すべて選択」は修正前と同じようにすべての行をチェックします。

## 修正後：「すべて選択」がinputでない場合に、独自のチェックボックスをmixedにする

```html
<form action="/invoices/download" method="post">
  <button type="button" class="check" id="select-all" role="checkbox" aria-checked="false">
    <span class="box"></span>
    すべて選択
  </button>
  <ul class="invoices">
    <li>
      <label class="check">
        <input type="checkbox" name="invoice" value="2026-03">
        2026年3月
      </label>
    </li>
    …
  </ul>
  <button type="submit">選択した請求書をダウンロード</button>
</form>
```

```css
/* ボックスとチェックマーク：行ではチェックボックスに、「すべて選択」ではspanに描く */
.check input,
.check .box { … }
.check input:checked,
.check[aria-checked="true"] .box { … }
/* 一部の行をチェック：チェックマークの代わりに横線 */
.check[aria-checked="mixed"] .box {
  border-color: #4f46e5;
  background: #4f46e5;
}
.check[aria-checked="mixed"] .box::before {
  visibility: visible;
  inline-size: 0.625rem;
  block-size: 0;
  border-width: 0 0 3px;
  rotate: none;
}
```

```js
const all = document.querySelector('#select-all');
const rows = [...document.querySelectorAll('input[name="invoice"]')];

// 「すべて選択」で、すべての行をチェックする。すべてチェック済みなら、すべて外す。
all.addEventListener('click', () => {
  const tick = all.getAttribute('aria-checked') !== 'true';
  rows.forEach((row) => { row.checked = tick; });
  all.setAttribute('aria-checked', String(tick));
});

// 行をチェックすると「すべて選択」を更新する。すべてならtrue、一部ならmixed。
rows.forEach((row) => row.addEventListener('change', () => {
  const ticked = rows.filter((r) => r.checked).length;
  all.setAttribute('aria-checked', ticked === rows.length ? 'true' : ticked ? 'mixed' : 'false');
}));
```

`aria-checked` は3つの値を取り、`"mixed"` は、一部だけチェックされたまとまりを表すチェックボックスのための値です。`role="checkbox"` を付けた `button` は、ボタン本来のフォーカスとSpaceキーでの操作がそのまま使え、中の「すべて選択」という文字が名前になります。スクリプトが3つの状態をすべて設定し、CSSがそこからチェックマークと横線を描くので、両者がずれることはありません。HTML標準のチェックボックスが自分でしていたことは、すべてこのスクリプトの仕事になります。チェックが変わるたびに `aria-checked` を更新し、ボタンを押したときに行をチェックしたり外したりする必要があります。「すべて選択」はフォームの入力欄ではなくなりますが、送るのは行だけなので、ここでは問題ありません。

## どの方法を選ぶか

どちらも基準を満たします。「すべて選択」がHTML標準のチェックボックスなら、`indeterminate` プロパティを使ってください。ほとんどの場合、標準のチェックボックスで作れます。今あるスクリプトに1行足すだけで、ブラウザーが一部チェックの状態を伝え、ボックスが押されればその状態を解除してくれるので、ほかに変えるところはありません。これが推奨する理由です。「すべて選択」がすでに独自のチェックボックスになっている場合（すべてのチェックボックスをボタンで描くコンポーネントライブラリーを使っている場合など）は、`aria-checked="mixed"` を使い、行をチェックするのと同じスクリプトで3つの状態をすべて設定します。横線のために、HTML標準のチェックボックスを独自のものに作り直さないでください。プロパティを使えば、そのままで横線も一部チェックの状態も得られます。

## 実装の判断

`indeterminate` はプロパティでしかないため、マークアップからは設定できず、リセットなどで元に戻ることもありません。ページを読み込んだときに、すでにチェックされている行から設定し、利用者が行をチェックしていないのに行の状態が変わるときにも設定し直します。フォームのリセットは各チェックボックスのチェックの状態を戻しますが `indeterminate` には触れないので、リセットの後にも設定します。一覧の絞り込みやページ送りの後、行を足したり消したりした後も同じです。`indeterminate` をマークアップに書くテンプレートやフレームワークでは何も起きません。描画のたびに参照（ref）から設定するなど、コンポーネントのスクリプトでプロパティを設定してください。

この例では、一部チェックのチェックボックスを押すとチェックが付きます。「すべて選択」は横線からチェックマークに変わり、すべての行がチェックされます。ARIA Authoring Practicesのチェックボックスのパターンには、押すたびにすべてチェック、すべて解除と進み、3回目で利用者が作った一部の選択に戻る「すべて選択」の例もあります。スクリプトは増えますが、どの段階でも伝える状態がボックスと一致していれば、どちらの動きでも4.1.2を満たします。

「一部」は、ボックスを淡く塗るだけでなく、横線や小さな四角といった専用の印で描きます。塗りだけでは一部となしを色でしか見分けられず、1.4.1 色の使用を満たしません。横線は、ここでのように枠線で描き、塗りつぶした図形では描かないでください。Windowsのコントラスト テーマでは、ブラウザーが背景の塗りを消し、枠線は利用者の配色で描き直します。枠線で描いた横線は残り、3つの状態を見分けられます。

行のチェックで「すべて選択」が変わっても、読み上げる必要はありません。フォーカスは行にあり、スクリーンリーダーは利用者が「すべて選択」に移ったとき、つまり必要なときにその状態を読みます。「4件中2件を選択」と伝えるライブリージョンを補助として加えてもかまいませんが、一部チェックの状態の代わりにはなりません。

## 修正の確認

1. スクリーンリーダーをオンにして1行をチェックし、「すべて選択」に戻って、横線が出ている間、一部だけチェックされた状態として読まれるか確認します。言い方はスクリーンリーダーによって異なります。
2. すべての行をチェックし、「すべて選択」がチェックマーク付きでチェックありと読まれるか確認します。すべて外し、空のボックスでチェックなしと読まれるか確認します。
3. 1行をチェックした状態で、「すべて選択」にフォーカスしてSpaceを押すと、すべての行がチェックされ、「すべて選択」はチェックありと読まれます。もう一度Spaceを押すと、すべての行が外れ、チェックなしと読まれます。
4. 一部の行をチェックした状態で、ブラウザーのアクセシビリティパネルで「すべて選択」のチェックの状態が「mixed」になっているか、Windowsのコントラスト テーマをオンにするか強制配色をエミュレートしても横線が見えるか確認します。

OS、ブラウザー、支援技術のバージョン、ビルド、日付、各手順の実際の結果を記録してください。一覧のコンポーネントやそのチェックボックスを変えたら、もう一度確認します。

## 制限

これは出発点となる実装例で、どの一覧でも動くことを保証するものではありません。ここで扱うのは「すべて選択」の状態です。キーボードでまったく届かないチェックボックスは「[キーボードでもスクリーンリーダーでも使えないカスタムチェックボックス](/ja/learn/custom-checkboxes)」で、選んだ選択肢が読み上げられない一覧は「[選択が読まれず、チップをキーボードで消せない複数選択](/ja/learn/multi-select)」で扱います。グループごとに「すべて選択」を持つ、入れ子のチェックボックスでは、どの階層にも同じ一部チェックの状態が必要です。利用者が使うスクリーンリーダーで、一覧全体を確認してください。

---

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