# 並べ替えの状態が矢印でしか分からない表

Source: https://easeweb.dev/learn/sortable-tables
Topics: 表とデータ > 並べ替えできる表が、どの列をどの向きで並べているかを伝える (WCAG 4.1.2)
The fix: 並べ替えのできる見出しには、列の名前のボタンを置きます。行を並べている列の th に aria-sort を付け、並べ替えのたびに付け替えます。矢印は aria-sort から描いてスクリーンリーダーからは隠し、ボタンのある見出しで並べ替えられることは、キャプションで一度だけ伝えます。
Test with: screen reader, keyboard
References: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F15 https://www.w3.org/WAI/ARIA/apg/patterns/table/examples/sortable-table/ https://www.w3.org/TR/wai-aria-1.2/#aria-sort https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-sort

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

## 場面

ネットショップのアカウントページで、注文履歴を表で一覧にしています。列は「注文」「日付」「合計」「状況」の四つです。表は日付の新しい順に並んだ状態で開き、「日付」の横に下向きの小さな矢印があります。「合計」を押すと金額の高い順に並べ替わり、矢印が「合計」の横へ移ります。もう一度押すと順番が逆になります。目で見る人には、矢印だけで十分に伝わります。

スクリーンリーダー利用者が見出しを順に移動すると、「日付」も「合計」も同じように読まれます。日付順に並んでいることも、新しい注文が先頭にあることも伝わらず、「合計」を押しても、並び順が変わったことや今どちらの向きなのかは聞こえません。よくある作り方の一つでは、見出しのセルそのものがクリックを受け付けます。この場合は普通の列見出しとして読まれるため、押せることさえ伝わりません。

## 確認できる問題

WCAG 4.1.2「名前（name）・役割（role）及び値（value）」は、ページ上のすべてのコントロールについて、支援技術が名前と役割を読み取れること、状態も読み取れることを求めています。並べ替えのできる見出しは、どの向きに並べているかという状態を持つコントロールです。この例では、その状態が目にしか伝わりません。矢印は、アイコンのライブラリーでよくあるように `aria-hidden="true"` を付けて描いたアイコンです。矢印を表示したり向きを変えたりする `sorted-desc` や `sorted-asc` のクラスは、ブラウザーにとって何の意味もありません。ARIAにはまさにこの状態のための `aria-sort` 属性があり、行を並べている列の見出しに付けます。どちらの作り方でも、この属性が付いていません。

sorttable.js のように見出しのセルそのもの、または TanStack Table の並べ替えの例のようにその中の `div` がクリックを受け付ける場合は、コントロールの役割もありません。見出しのセルはボタンではないため、スクリーンリーダーは見出しとして読み、何かができる要素だとは伝えません。これは、アクセシビリティAPIを使わないカスタムコントロールの失敗例F15にあたります。クリックの処理はどの要素にも付けられ、描いた矢印を状態と結び付ける仕組みはHTMLにないため、ブラウザーはこの作りをそのまま受け入れてしまいます。

コントロールの名前には注意点があります。見出しの文字は列の名前で、その列のすべてのセルと一緒に読まれるため、「並べ替え」という言葉まで入れることはできません。ARIA Authoring Practicesの例では、代わりに表のキャプションで一度だけ「ボタンのある列見出しで並べ替えられます」と伝えています。

## 利用者への影響

長い表を並べ替えるのは、金額の大きい注文、支払いの済んでいない一番古い請求書、一番早く届く商品など、探しているものを見つけるためです。並び順が伝わらなくても、スクリーンリーダー利用者は並べ替えること自体はできます。ただ、何が起きたのかを知るには、何行か読んで比べる必要があります。

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

axe も ANDI も並べ替えの状態は確かめないため、矢印のアイコンだけの見出しも、`aria-sort` のないボタンの見出しも合格になります。並べ替えの印が画像だけなら、どの列で並べているかはコードから分かりません。`sorted-desc`、`sorting_asc`、`sorttable_sorted` のようにクラスが状態を表している場合は、表のどの見出しにも `aria-sort` がなければ、この診断は問題として報告します。見出しで並べ替えられそうな表には、スクリーンリーダーで押して確かめる確認候補も出します。`onclick` 属性でクリックを受け付けるのにTabキーでは届かない見出しは、キーボードで到達できないコントロールとして報告します。sorttable.js のようにスクリプトからクリックの処理を付けた見出しは、この確認候補で人が確かめます。TanStack Table の並べ替えの例や shadcn/ui のデータ表など、よく使われるライブラリーの例は、矢印を描くだけで `aria-sort` を付けていません。こうした例に倣うAIのコード生成も、同じものを書くことがあります。

## 修正前：見出しのセルがクリックを受け付ける

```html
<table class="orders">
  <caption>Your orders</caption>
  <thead>
    <tr>
      <th scope="col">Order</th>
      <th scope="col" class="sorted-desc" data-key="date">
        Date
        <svg class="arrow" aria-hidden="true">…</svg>
      </th>
      <th scope="col" data-key="total">
        Total
        <svg class="arrow" aria-hidden="true">…</svg>
      </th>
      <th scope="col">Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>#1071</td>
      <td>22 Oct 2026</td>
      <td>$9.90</td>
      <td>Processing</td>
    </tr>
    …
  </tbody>
</table>
```

```css
.orders .arrow {
  visibility: hidden;
}
.orders .sorted-asc .arrow,
.orders .sorted-desc .arrow {
  visibility: visible;
}
.orders .sorted-asc .arrow {
  rotate: 180deg;
}
```

```js
for (const th of document.querySelectorAll('.orders th[data-key]')) {
  th.addEventListener('click', () => sortBy(th));
}

function sortBy(th) {
  const dir = th.classList.contains('sorted-desc') ? 'asc' : 'desc';
  for (const other of document.querySelectorAll('.orders th')) {
    other.classList.remove('sorted-asc', 'sorted-desc');
  }
  th.classList.add(`sorted-${dir}`);
  renderRows(th.dataset.key, dir);
}
```

- 「Date」と「Total」の見出しはセル自体がクリックを受け付けるため、ボタンではなく列見出しとして読まれ、押せることが伝わらない。Tabキーで届かないことは別の問題で、[Tabキーで到達できないdivのボタン](/ja/learn/native-buttons)で扱う。
- 矢印はスクリーンリーダーから隠したアイコンで、ブラウザーが何も知らないクラスで表示や向きを切り替えているため、並べ替えは目にしか伝わらない。
- `aria-sort` の付いた見出しがないため、並べている列も向きも読まれない。

## 修正前：見出しにボタンはあるが、aria-sortがない

```html
<table class="orders">
  <caption>Your orders</caption>
  <thead>
    <tr>
      <th scope="col">Order</th>
      <th scope="col" class="sorted-desc">
        <button type="button" data-key="date">
          Date
          <svg class="arrow" aria-hidden="true">…</svg>
        </button>
      </th>
      <th scope="col">
        <button type="button" data-key="total">
          Total
          <svg class="arrow" aria-hidden="true">…</svg>
        </button>
      </th>
      <th scope="col">Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>#1071</td>
      <td>22 Oct 2026</td>
      <td>$9.90</td>
      <td>Processing</td>
    </tr>
    …
  </tbody>
</table>
```

```css
.orders .arrow {
  visibility: hidden;
}
.orders .sorted-asc .arrow,
.orders .sorted-desc .arrow {
  visibility: visible;
}
.orders .sorted-asc .arrow {
  rotate: 180deg;
}
```

```js
for (const button of document.querySelectorAll('.orders th button')) {
  button.addEventListener('click', () => sortBy(button));
}

function sortBy(button) {
  const th = button.closest('th');
  const dir = th.classList.contains('sorted-desc') ? 'asc' : 'desc';
  for (const other of document.querySelectorAll('.orders th')) {
    other.classList.remove('sorted-asc', 'sorted-desc');
  }
  th.classList.add(`sorted-${dir}`);
  renderRows(button.dataset.key, dir);
}
```

ボタンは正しく作られています。Tabキーで届き、EnterキーやSpaceキーで押せて、「Date、ボタン」と読まれます。問題は並べ替えの状態です。

- 日付順に並んでいることも、新しい注文が先頭にあることも読まれない。
- 「Total」を押すとクラスが移って矢印の向きも変わるが、スクリーンリーダーには「Total、ボタン」としか聞こえない。
- ボタンで表を並べ替えられることが伝わらない。

## 修正後：各見出しのボタンと、並べている列のaria-sort

```html
<table class="orders">
  <caption>
    Your orders
    <span class="visually-hidden">, column headers with buttons are sortable</span>
  </caption>
  <thead>
    <tr>
      <th scope="col">Order</th>
      <th scope="col" aria-sort="descending">
        <button type="button" data-key="date">
          Date
          <svg class="arrow" aria-hidden="true">…</svg>
        </button>
      </th>
      <th scope="col">
        <button type="button" data-key="total">
          Total
          <svg class="arrow" aria-hidden="true">…</svg>
        </button>
      </th>
      <th scope="col">Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>#1071</td>
      <td>22 Oct 2026</td>
      <td>$9.90</td>
      <td>Processing</td>
    </tr>
    …
  </tbody>
</table>
```

```css
.orders .arrow {
  visibility: hidden;
}
.orders [aria-sort] .arrow {
  visibility: visible;
}
.orders [aria-sort="ascending"] .arrow {
  rotate: 180deg;
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
```

```js
for (const button of document.querySelectorAll('.orders th button')) {
  button.addEventListener('click', () => sortBy(button));
}

function sortBy(button) {
  const th = button.closest('th');
  const dir = th.getAttribute('aria-sort') === 'descending' ? 'ascending' : 'descending';
  for (const other of document.querySelectorAll('.orders th')) {
    other.removeAttribute('aria-sort');
  }
  th.setAttribute('aria-sort', dir);
  renderRows(button.dataset.key, dir);
}
```

1. 並べ替えのできる見出しの文字を、列の名前だけを名前にした `button` に入れる。Tabキーで届き、EnterキーやSpaceキーで押せて、「Date、ボタン」と読まれる。
2. ページを開いた時点で「Date」の見出しに `aria-sort="descending"` を付け、押すたびに付け替える。すべての見出しから外してから、押された見出しに ascending か descending を付ける。スクリーンリーダーは見出しと一緒に、その列で並べていることと向きを読み上げる。
3. 矢印はクラスではなく `aria-sort` から描き、見た目と読み上げが食い違わないようにする。アイコンの `aria-hidden` はそのまま残し、ボタンの名前に何も加えないようにする。
4. キャプションに、見えないように隠した補足 `, column headers with buttons are sortable` を加える。見出しごとに言葉を足さなくても、表全体で一度、見出しで並べ替えられることが伝わる。

これで二つの修正前の例はどちらも直り、4.1.2を満たします。「Order」と「Status」にはボタンがないため、`aria-sort` も付けません。

## 実装の判断

**見出しの名前に「並べ替え」を入れない。** 見出しの名前は、その下のセルと一緒に読まれます。ボタンの名前を「日付で並べ替え」にすると、列を下へ移動するたびに、日付の前に「日付で並べ替え」と読まれます。「Date: Activate to invert sorting」のようなラベルを見出しのセル自体に付けるライブラリーもあり、同じことが起きます。キャプションの補足なら一度で済みます。

**並べる列は一度に一つ。** ARIAは、一つの表で `aria-sort` を付ける見出しは一度に一つだけにするよう求めています。並べていない列には付けません。属性がなければ `none` と同じ意味になります。状況で並べ、同じ状況の中を日付で並べる表なら、状況の列に付け、二つ目の基準はキャプションで伝えます。`aria-sort="other"` は、利用者が自分で決めた順のように、昇順でも降順でもない並べ方に使います。

**新しい並び順をすぐ伝える。** ボタンを押してもフォーカスはボタンに残ります。変わった `aria-sort` は、いったん見出しを離れて戻るまで読まれないことがあります。すぐに伝える必要があるときは、「合計の高い順に並べ替えました」のような短いメッセージも、ページにあらかじめ置いた polite のステータス領域に書き込みます。[画面に出ても読み上げられないステータスメッセージ](/ja/learn/status-messages)と同じ方法です。言い方は見出しに合わせ、`aria-sort` の代わりにはしないでください。表に戻ってきた人が読むのは `aria-sort` です。

**リンクで並べ替える場合。** `?sort=total` のようなリンクでページを読み込み直し、サーバーで並べ替える表では、見出しに置くのはボタンではなくリンクです。読み込まれたページでも、並べている列に `aria-sort` を付け、ページのタイトルや見出しで並び順を伝えられます。

**向きは矢印の形で伝える。** 二つの三角を並べて片方だけ色を変えるのではなく、行の並ぶ向きを指す矢印を一本描きます。向きを色だけで伝えると、その色が見えない人には上下の区別が付かず、1.4.1「色の使用」を満たしません。下のデザインの案に、基準を満たす描き方を示しています。

**Windowsのコントラスト テーマでの矢印。** 矢印は `currentColor` か文字で描きます。テーマが色を置き換えても見出しと同じ色を保てます。背景画像で描くと、ファイルでない画像はテーマに外され、ファイルの画像は元の色のまま残ります。

見出しそのものは別の問題で、[見出しが太字だけのデータ表](/ja/learn/table-headers)で扱っています。表の名前を付けるキャプションは[表題がキャプションになっていない表](/ja/learn/table-captions)で扱っています。三つとも同じ表のコンポーネントで対応してください。

## 修正の確認

1. コードで、表を並べ替える見出しをすべて探し、見出しの文字が列の名前だけを名前にしたボタンに入っていて、見出しのセル自体はクリックを受け付けていないことを確認する。
2. `aria-sort` の付いた `th` がちょうど一つで、行を並べている列に、矢印と同じ向きで付いていること、別の列で並べ替えると付け替わることを確認する。
3. スクリーンリーダーで、並べ替えのできる見出しへTabキーで移動する。どれも列の名前のボタンとして読まれ、並べている列は、描いたとおりの昇順か降順で並べ替え済みと読まれることを確認する。
4. 「Total」をEnterキーで押し、いったん離れてから戻る。今の並び順の向きで並べ替え済みと読まれ、「Date」はもう並べ替え済みと読まれないことを確認する。
5. スクリーンリーダーの表の移動キー（NVDAとJAWSではT）で表へ移動し、キャプションが、見出しで並べ替えられるという補足と一緒に読まれることを確認する。

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

## 制限

このサンプルは、どの表でも同じ結果になることを保証するものではありません。`renderRows` は、表の本体を描き直す実際のコードに置き換えてください。`aria-sort` の読み方はスクリーンリーダーによって異なり、押したときには読まず、フォーカスが見出しのセルに入ったときにだけ読むこともあります。並べ替えと同時に行を絞り込んだり、検索で行が減ったりする場合、表示している行数を伝えることは別の問題で、[件数が読み上げられない検索・絞り込み結果](/ja/learn/search-results)で扱っています。

---

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