データ表示

グリッド

解説あり

Grid

セル単位でキーボード操作できる対話的な表(データグリッド)。

グリッド(データグリッド)とは?

グリッドは、表形式のデータをキーボードの矢印キーで自由に動き回れるように作った、対話的な UI です。表計算ソフトのように「セルからセルへ」フォーカスを移しながら 操作できるのが特徴です。

ここで言うグリッドは、CSS のレイアウト用 grid とは別物です。 また、ただ並べて見せるだけの表なら <table> を使うべきで、 グリッドにする必要はありません。「セル単位でフォーカスを動かしたい」「セル内に操作要素がある」 といった対話が必要なときにだけグリッドを使います。

なぜアクセシビリティが大事なの?

表を「触れるもの」にするほど、設計を誤ると操作不能な人が出ます。

解決策は、role="grid" で構造を宣言し、矢印キーでフォーカスを動かす (ローミング tabindex)ことです。グリッド全体を1つのタブストップにして、 中は矢印で移動させます。

ライブデモ(推奨実装)

下の社員一覧は APG に沿った対話的グリッドです。マウスを使わずTab で一度フォーカスを入れたら、あとは矢印キーだけで セル間を移動してみてください。

アクセシブルなデータグリッド

社員一覧

社員ID氏名部署内線
E-001佐藤 花子営業部1201
E-002鈴木 太郎開発部1310
E-003高橋 美咲人事部1105
E-004田中 健開発部1322

試してみよう:Tab でグリッドに入る → ↑ ↓ ← → でセル移動 → Home / End で行の先頭・末尾、Ctrl + Home / Ctrl + End でグリッドの先頭・末尾。

ポイント

スクリーンリーダー(macOSなら +F5 で VoiceOver)をオンにすると、 セルに入ったとき「グリッド, 4列, 5行」のように構造が読み上げられ、移動するたびに 「氏名 列, 佐藤 花子」のように列見出しとセルの値が読まれます。 これが role="grid" と列見出しの効果です。

キーボード操作

キー動作必須/任意
Tabグリッド全体に出入りする(中は1タブストップ)必須
/ 右 / 左のセルへフォーカス移動必須
/ 下 / 上のセルへフォーカス移動必須
Home / Endその行の先頭 / 末尾のセルへ必須
Ctrl+Home / Ctrl+Endグリッドの先頭 / 末尾のセルへ任意(推奨)

補足

グリッドの中で Tab を「セル移動」に割り当ててはいけません。Tab はあくまでグリッドの外へ出るためのキーにし、セル移動は矢印キーに任せます。これがネイティブな表との大きな違いです。

必要な WAI-ARIA / ロール

付ける場所属性 / ロール意味
外枠role="grid"対話的なグリッドであることを宣言する。
外枠aria-label / aria-labelledbyグリッドに名前を付ける(何の表かを伝える)。
各行role="row"行のまとまりを示す。
見出しセルrole="columnheader"列の見出し。各列のセルと関連付けて読まれる。
データセルrole="gridcell"1つ1つのセル。
フォーカスするセルtabindex="0"(1つだけ)/ "-1"(残り全部)ローミング tabindex。グリッドを1タブストップにし、移動時に付け替える。

実装:推奨パターン(Good)

良い例 / 推奨

role="grid" で構造を宣言し、常に1つのセルだけ tabindex="0"にして矢印キーでフォーカスを動かします(ローミング tabindex)。

マークアップ:

<div role="grid" aria-labelledby="grid-cap">
  <div role="row">
    <span role="columnheader" tabindex="-1">社員ID</span>
    <span role="columnheader" tabindex="-1">氏名</span>
    <span role="columnheader" tabindex="-1">部署</span>
    <span role="columnheader" tabindex="-1">内線</span>
  </div>
  <div role="row">
    <!-- グリッド全体で最初のセルだけ tabindex="0"(残りは -1) -->
    <span role="gridcell" tabindex="0">E-001</span>
    <span role="gridcell" tabindex="-1">佐藤 花子</span>
    <span role="gridcell" tabindex="-1">営業部</span>
    <span role="gridcell" tabindex="-1">1201</span>
  </div>
  <!-- 以降の行も role="row" + role="gridcell" tabindex="-1" を並べる -->
</div>

キーボード操作のスクリプト(フォーカス移動と tabindex の付け替えがすべて):

const grid = document.getElementById('grid-good');
if (grid) {
  // 2次元配列としてセルを把握する(行ごとのセル配列)
  const rows = Array.from(grid.querySelectorAll<HTMLElement>('[role="row"]'));
  const cells = rows.map((row) =>
    Array.from(row.querySelectorAll<HTMLElement>('[role="columnheader"],[role="gridcell"]'))
  );

  let r = 0; // 現在の行
  let c = 0; // 現在の列

  // 現在位置のセルだけ tabindex="0"、他は -1 にして 1タブストップにする
  function focusCell(nextR: number, nextC: number) {
    const maxR = cells.length - 1;
    r = Math.max(0, Math.min(nextR, maxR));
    const maxC = cells[r].length - 1;
    c = Math.max(0, Math.min(nextC, maxC));
    cells.flat().forEach((cell) => cell.setAttribute('tabindex', '-1'));
    const target = cells[r][c];
    target.setAttribute('tabindex', '0');
    target.focus();
  }

  grid.addEventListener('keydown', (e) => {
    switch (e.key) {
      case 'ArrowRight': focusCell(r, c + 1); break;
      case 'ArrowLeft':  focusCell(r, c - 1); break;
      case 'ArrowDown':  focusCell(r + 1, c); break;
      case 'ArrowUp':    focusCell(r - 1, c); break;
      case 'Home':
        if (e.ctrlKey) focusCell(0, 0);            // 先頭セルへ
        else focusCell(r, 0);                       // 行の先頭へ
        break;
      case 'End':
        if (e.ctrlKey) focusCell(cells.length - 1, // 末尾セルへ
          cells[cells.length - 1].length - 1);
        else focusCell(r, cells[r].length - 1);    // 行の末尾へ
        break;
      default: return; // 関係ないキーはブラウザに任せる
    }
    e.preventDefault();
  });

  // クリックしたセルにも現在位置を合わせる
  cells.forEach((row, ri) =>
    row.forEach((cell, ci) => {
      cell.addEventListener('click', () => focusCell(ri, ci));
    })
  );
}

補足

セルの中にボタンやリンクを置く場合は、まずセル間を矢印で移動できるようにし、EnterF2 で「セル内の操作モード」に入る、といった拡張を検討します。 まずは1セル=1フォーカスの基本形を確実に動かすことが先決です。

アンチパターン(Bad)

下は <table> のセルに onclick を付けただけの「対話できるつもり」の表です。マウスでは選べますが、キーボードでは一切操作できません。上のデモと同じように Tab → 矢印キーを試して、違いを体感してください。

onclick だけで作った壊れたグリッド
社員ID氏名部署内線
E-001佐藤 花子営業部1201
E-002鈴木 太郎開発部1310
E-003高橋 美咲人事部1105
E-004田中 健開発部1322

試してみよう:Tab を押してもセルにフォーカスが入らず、矢印でも動けません。スクリーンリーダーでは「グリッド」とも認識されず、行・列・現在位置が伝わりません。

<!-- ❌ アンチパターン -->
<!-- 見た目は表だが「対話的グリッド」としては成立していない -->
<table>
  <tr>
    <th>社員ID</th><th>氏名</th><th>部署</th><th>内線</th>
  </tr>
  <tr>
    <!-- onclick だけ。role も tabindex もキー処理も無い -->
    <td onclick="select(this)">E-001</td>
    <td onclick="select(this)">佐藤 花子</td>
    <td onclick="select(this)">営業部</td>
    <td onclick="select(this)">1201</td>
  </tr>
</table>

悪い例 / 避ける

この実装の問題点:

  • キーボードで操作できない — セルに tabindex もキー処理も無く、矢印で動けない。
  • グリッドと認識されないrole="grid" が無く、支援技術には「ただの表」に聞こえる。
  • 1タブストップになっていない — ローミング tabindex の設計が無い。
  • 現在位置が伝わらない — 何行目・何列目にいるのか、選択状態も読み上げられない。

ポイント

そもそもクリックやキー操作が不要な表なら、無理にグリッドにせず ネイティブの <table><th scope> 付き)で十分です。 「セル単位の対話」が本当に必要なときだけ、role="grid" とローミング tabindex を導入しましょう。

実装チェックリスト


原文(英語):Grid Pattern — W3C APG(新しいタブで開きます)