ソート可能テーブル
列ヘッダーをタップすると昇順/降順に並び替わり、矢印で現在の方向を表示する。日本語ダミーデータ5行。
プレビュー
操作クリック/タップで動きます。
使いどころ
用途は絞り込みより並べ替えが主目的の一覧が対象。在庫リスト、ランキング、注文一覧など、数値やテキストの大小・五十音で並べ替えると「一番高い/一番安い」を探しやすくなる場面に向く。逆に、行数が数百件を超えるデータや、複数条件でのフィルタリングが必要な一覧には力不足で、ソートだけでは目的の行にたどり着けない。デモは3列・5行だが、実運用では列数が増えるほど「今どの列でソートしているか」が見えにくくなるため、アクティブな列のヘッダーを背景色や太字で常時強調しておくと迷わない。table-layout: fixedで列幅を固定しているため、商品名のような可変長テキストを扱う列は幅の見積もりを先に決めておく必要があり、幅が狭いレイアウトでは横スクロールできるラッパーで包む前提で設計するとよい。
仕組み
各th要素にdata-key(ソートに使うプロパティ名)とdata-type(str/num)を持たせ、クリックまたはEnter/SpaceキーでsortByを呼ぶ。同じ列を再度押すと昇順⇄降順をトグルし、別の列を押すと新規に昇順から始まる。矢印記号は直接書き換えず、st-th[aria-sort="ascending"] .st-arrow::after { content: " ↑"; } のようにaria-sort属性依存のCSS ::after contentで描画しており、JSはDOM属性を切り替えるだけでよい設計になっている。行の再描画はrenderRows()が既存の<tr>要素を可能な限り再利用し、textContentだけを差し替える方式で、DOM生成コストを抑えている。文字列比較はlocaleCompare(bv, "ja")を使っており、単純な文字コード比較よりも日本語の五十音順に近い並びになる。
実装の注意
初期表示では矢印が無く「この表は並べ替えできる」ことが視覚的に伝わりにくい(薄い上下矢印を常時出すなどの手当てが要る。なおaria-sortを初期状態から全列に付ける必要はない——後述のとおり既定値がnoneのため)。次に、thはtabindex="0"でフォーカス可能にしてあるがrole="button"や適切な意味づけが無いため、支援技術には単なる見出しセルとして扱われ、「押せる」ことがラベルから伝わりにくい。keydownでEnterとSpaceを自前ハンドリングしているのはよいが、Space時にe.preventDefault()を呼び忘れるとページスクロールが同時に起きる(このデモではpreventDefault()できている)。ソート中のアニメーションが一切ないため、行数が多いテーブルを切り替えると視線が追いにくいのも実務では気になりやすい。
アクセシビリティ
aria-sortの扱いはこの実装が正しい。属性の既定値はnoneなので未ソートのthに明示する必要はなく、WAI-ARIAはむしろ「一度に1つのヘッダーにだけ適用する」ことを推奨している——全thにaria-sort="none"を並べると「どの列が今の並び順か」という一意性の意味が薄れるため、現在の「能動列にだけ付け、他からは外す」動きを崩さないこと。クリック可能なthは見た目のcursor: pointerだけでなく、実務ではth内に<button>を入れてボタン化するのが定石で、そうすれば支援技術がクリック可能と自動的に認識し、フォーカスリングやEnter/Spaceの既定動作もブラウザに任せられる。現状のtabindex="0"+自前keydownでも操作自体は成立するが、role="button"を明示するかbutton化するかのどちらかで「押せる見出し」であることを補うべきだ。並べ替え結果(何位が何位に動いたか)は視覚的にしか伝わらないため、tbodyにaria-live="polite"を足して件数や先頭行の変化を読み上げさせる改善余地もある。
調整して使う
触れる点は主に3つ。ヘッダーの色(既定は#2b50c7の藍、hoverは#1a3aaa)はブランドカラーに応じて変えられ、hoverで一段暗くする関係を保てばクリック可能性が伝わりやすい。次に矢印の見せ方で、::afterのテキスト矢印(" ↑"/" ↓")の代わりにSVGアイコンへ差し替えると、フォントによる太さの不揃いを避けられる。最後にソート対象の型で、デモはstr/numの2種類だが、日付列を扱うにはdata-type="date"を足してnew Date()での比較分岐を加えるだけで拡張できる。行の背景を偶数/奇数で塗り分けるゼブラストライプ(nth-child(even)/(odd))は既に入っており、行数が増える運用ほどこの効果が効いてくる。
コード(コピーして使えます)
<div class="st-wrap">
<table class="st-table" aria-label="売上テーブル">
<thead>
<tr>
<th class="st-th" data-key="name" data-type="str" tabindex="0">
商品<span class="st-arrow" aria-hidden="true"></span>
</th>
<th class="st-th" data-key="sales" data-type="num" tabindex="0">
売上<span class="st-arrow" aria-hidden="true"></span>
</th>
<th class="st-th" data-key="rank" data-type="num" tabindex="0">
順位<span class="st-arrow" aria-hidden="true"></span>
</th>
</tr>
</thead>
<tbody id="stBody"></tbody>
</table>
</div>