はじめに
Chrome 150から、HTML属性の focusgroup が使えるようになりました。
これまでツールバーやタブリストなどの複合ウィジェットでキーボードナビゲーションを実装するには、JavaScriptで「roving tabindex」パターンを手書きする必要がありました。focusgroup 属性を使えば、そのロジックをJavaScriptなしでHTMLだけで宣言的に書けます。
これまでの課題
WAI-ARIAのベストプラクティスでは、ツールバーやメニューなどの複合ウィジェットにはroving tabindexを実装することが推奨されています。
roving tabindexとは「現在フォーカスされている要素だけ tabindex="0" にし、他の要素は tabindex="-1" にして、矢印キーで移動するたびにJavaScriptで付け替える」パターンです。
const items = document.querySelectorAll('[role="tab"]');
items.forEach((item, index) => {
item.addEventListener('keydown', (e) => {
let next;
if (e.key === 'ArrowRight') next = items[(index + 1) % items.length];
if (e.key === 'ArrowLeft') next = items[(index - 1 + items.length) % items.length];
if (next) {
item.setAttribute('tabindex', '-1');
next.setAttribute('tabindex', '0');
next.focus();
}
});
});
シンプルな例でもこれだけのコードが必要で、折り返しや方向制御などを加えると一気に複雑になります。
focusgroup属性で解決
focusgroup 属性を親要素に付けるだけで、ブラウザが矢印キーナビゲーションを自動で処理してくれます。
<div focusgroup="toolbar" aria-label="書式設定">
<button>太字</button>
<button>斜体</button>
<button>下線</button>
</div>
これだけで
- 左右矢印キーでボタン間を移動できる
- Tabキーで初回フォーカス(または最後にフォーカスしていた要素)に戻る
- roving tabindexのJavaScriptが不要
基本的な使い方
ビヘイビアトークン
ウィジェットの種類に応じたプリセットが用意されています。それぞれデフォルトの方向や折り返しが設定されています。
| 値 | 矢印キーの方向 | 折り返し |
|---|---|---|
toolbar |
左右 | なし |
tablist |
左右 | あり |
menubar |
左右 | あり |
menu |
上下 | あり |
radiogroup |
全方向 | なし |
listbox |
全方向 | なし |
<!-- ツールバー -->
<div focusgroup="toolbar" role="toolbar" aria-label="書式設定">
<button>太字</button>
<button>斜体</button>
<button>下線</button>
</div>
<!-- タブリスト(左右+折り返し) -->
<div focusgroup="tablist" role="tablist">
<button role="tab">タブ1</button>
<button role="tab">タブ2</button>
<button role="tab">タブ3</button>
</div>
<!-- 縦メニュー(上下+折り返し) -->
<ul focusgroup="menu" role="menu">
<li role="menuitem">メニュー1</li>
<li role="menuitem">メニュー2</li>
<li role="menuitem">メニュー3</li>
</ul>
修飾子トークン
ビヘイビアトークンに組み合わせてデフォルト挙動を上書きできます。
方向の指定:
-
inline— 左右矢印キーのみ反応 -
block— 上下矢印キーのみ反応
折り返しの制御:
-
wrap— 末尾まで進んだら先頭にループ -
nowrap— ループを無効化
フォーカス記憶の制御:
-
nomemory— 最後にフォーカスした要素を記憶しない(Tabで常に先頭に戻る)
<!-- toolbarに折り返しを追加 -->
<div focusgroup="toolbar wrap" aria-label="書式設定">
<button>太字</button>
<button>斜体</button>
<button>下線</button>
</div>
<!-- tablistのフォーカス記憶を無効化 -->
<div focusgroup="tablist nomemory" role="tablist">
<button role="tab">タブ1</button>
<button role="tab">タブ2</button>
</div>
グループからの除外
特定の要素をfocusgroupのナビゲーション対象から外したいときは none を使います。
<div focusgroup="toolbar">
<button>太字</button>
<button>斜体</button>
<span focusgroup="none">
<button>ヘルプ</button>
</span>
</div>
デモ
実際の動きを確認してみてください。
See the Pen Focusgroup by degudegu2510 | Qiita (@degudegu2510) on CodePen.
ブラウザサポート
Chrome 150以降でサポート。現時点ではChrome/Edgeのみで、FirefoxとSafariは未対応です。
まとめ
focusgroup 属性を使うことで、これまでJavaScriptで手書きしていたroving tabindexのロジックをHTMLだけで表現できます。WAI-ARIAのロールに対応したプリセット(toolbar、tablist、menu など)が用意されているので、意味的にも正しいマークアップと組み合わせやすいのが嬉しいポイントです。
アクセシブルなキーボードナビゲーションの実装コストが下がるのは、Webにとって大きな前進だと思います。ぜひ試してみてください!