ウィジェットの中の矢印キー:roving tabindexとaria-activedescendant
タブ、ツールバー、リストボックス、グリッドをTabの移動先1つにまとめ、中を矢印キーで動かす2つの方法と、それぞれが合う場面
要点:複合ウィジェットはTabの移動先を1つにし、中は矢印キーで移動します。方法は2つあります。roving tabindexで実際のフォーカスを動かす(アクティブな項目に tabindex="0"、ほかに "-1")か、フォーカスはコンテナーや入力欄に置いたまま aria-activedescendant でアクティブな項目を指すかです。編集できるコンボボックスでは、入力欄にフォーカスを残す必要があるため、後者を使います。
01
使う場面
再現したメモアプリに、4つのボタン(元に戻す、やり直す、リンクを挿入、画像を挿入)が並ぶ書式のツールバーと、入力に合わせてタグを提案するタグ欄があります。「デザ」と入力すると、「デザイン」「デザインレビュー」「デザイン案」が出ます。キーボードでのテストから、2つの指摘が返ってきました。
- Tabキーで「元に戻す」には移動できるが、そこで終わる。ほかの3つのボタンにはフォーカスが移らず、矢印キーも効かない。
- タグ欄で下矢印キーを押すと、フォーカスがリストへ移る。そのあと入力した文字がどこにも入らない。
どちらも、1つのTabの移動先の中に部品がいくつもあるウィジェットです。中で矢印キーをどう動かすか、2つの方法のどちらを使うかを決める必要があります。
02
Tabの移動先は1つ、中は矢印キー
ARIA Authoring Practices Guide(APG)は、どのプラットフォームにも共通する決まりとして、TabキーとShift + Tabはコンポーネントからコンポーネントへフォーカスを移し、フォーカス可能な要素をいくつも含むコンポーネントの中では、主に矢印キーがフォーカスを移す、と説明しています。WAI-ARIAはこうしたコンポーネントを複合ウィジェットと呼び、中のフォーカスを制御することを「フォーカスの管理」と呼びます。APGは、Tabの移動順に含めるのは、複合ウィジェットの中のフォーカス可能な要素1つだけにするよう求めています。
Tabキーでウィジェットに入ったときにフォーカスが移る先は、ウィジェットの種類で決まっています(APG「コンポーネントの中のキーボード移動」)。
- 前回フォーカスがあった要素(まだなければ最初の要素):グリッド、ツリーグリッド。
- 選択中の要素(なければ最初の要素):ラジオグループ(チェックされたボタン)、タブ、リストボックス、ツリー。
- 最初の要素: メニューバー、ツールバー。ツールバーのパターンでは、前回フォーカスがあった要素にしてもよいとしています。
中で使うキーは、APGのパターンごとに決まっています。APGは、どのキーを使うかは作成者が決めてよいものの、一般的なOSの同じ種類の部品と同じキーにするよう強く勧めています。
| パターン | 矢印キー | HomeとEnd |
|---|---|---|
| タブ(横並び) | 左右で移動し、最後から最初へ、最初から最後へ回る。上下はページのスクロールに残す | 任意。最初と最後のタブへ |
| ツールバー(横並び) | 左右で移動。端で回るかは任意 | 任意。最初と最後のコントロールへ |
| ラジオグループ | 上下左右でフォーカスを移してチェックし、端で回る | パターンにはない |
| リストボックス | 上下で移動 | 任意。選択肢が6つ以上なら強く推奨 |
| ツリービュー | 上下で移動。右で開くか中へ、左で閉じるか親へ | 最初と最後のノードへ |
| グリッド | 上下左右で1セルずつ | HomeとEndで行の端へ、Ctrl + HomeとCtrl + Endで最初と最後のセルへ |
リストボックスとツリーでは、文字を入力すると、その文字で始まる次の項目へ移るタイプアヘッドも勧められています。各パターンを実際に試せるデモは、「マウスでしか切り替えられないタブ」「キーボードで押せないアイコンが並ぶツールバー」「キーボードでもスクリーンリーダーでも選べない星評価」(ラジオグループ)、「キーボードで開けず、階層も伝わらないツリービュー」、「キーボードでセルを移動・編集できないデータグリッド」と「キーボードで選べず、読み上げもされない座席表」(どちらもグリッド)にあります。
03
roving tabindex:実際のフォーカスを動かす
roving tabindexでは、Tabの移動順に含める項目を tabindex="0" にし、ウィジェットの中のほかの項目をすべて tabindex="-1" にします。-1 の項目はTabの移動順から外れますが、スクリプトからはフォーカスできます。APGの手順は次のとおりです。
- ウィジェットを作るとき、最初の項目に
tabindex="0"、ほかの項目にtabindex="-1"を付けます。 - 矢印キーが押されたら、
0を持つ項目をtabindex="-1"にし、フォーカスを移す先の項目をtabindex="0"にします。 - その項目で
focus()を呼びます。
使う場面のツールバーを、この方法で作ると次のようになります。
<div role="toolbar" aria-label="書式">
<button type="button" tabindex="0">元に戻す</button>
<button type="button" tabindex="-1">やり直す</button>
<button type="button" tabindex="-1">リンクを挿入</button>
<button type="button" tabindex="-1">画像を挿入</button>
</div>const toolbar = document.querySelector('[role="toolbar"]');
const tools = [...toolbar.querySelectorAll('button')];
// Tabの移動先を、このボタンだけにします。
function setStop(tool) {
for (const t of tools) t.tabIndex = t === tool ? 0 : -1;
}
toolbar.addEventListener('keydown', (event) => {
const i = tools.indexOf(document.activeElement);
const to = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tools.length - 1 }[event.key];
if (to === undefined) return;
event.preventDefault();
const tool = tools[(to + tools.length) % tools.length];
setStop(tool);
tool.focus();
});
// クリックしたボタンにフォーカスを移すブラウザーでは、クリックでもTabの移動先を移します。
toolbar.addEventListener('focusin', (event) => {
if (tools.includes(event.target)) setStop(event.target);
});フォーカスが実際に移るため、ブラウザーが項目を画面内へスクロールし、フォーカスの枠も描き、:focus-visible のスタイルも効きます。APGは、このスクロールをこの方法の利点の一つに挙げています。各項目はHTML標準のキー操作も保つので、ボタンはEnterキーでもSpaceキーでも押せます。APGはさらに、クリックやタップでもキーと同じように tabindex を設定するよう求めています。そうしないと、Shift + Tabでウィジェットに戻ったとき、利用者が離れた場所とは別の項目に戻ってしまいます。
04
aria-activedescendant:フォーカスは残し、アクティブな項目を動かす
aria-activedescendant では、Tabの移動順に入るのはコンテナー(またはそれを制御する入力欄)だけで、DOMのフォーカスもそこに残ります。値はアクティブな項目のidです。WAI-ARIA 1.2の4.3節によれば、ブラウザーはDOMのフォーカスをコンテナーか、それを制御する入力欄に置いたまま、aria-activedescendant が指す要素にフォーカスがあるものとして、フォーカスのイベントと状態を支援技術に伝えます。キーのイベントはDOMのフォーカスがある要素に届くので、スクリプトもそこで処理します。
その代わり、次の3つは作成者の仕事になります。
- 強調の表示: WAI-ARIA 1.2の4.3.1節は、アクティブな項目のスタイルはユーザーエージェントではなく作成者が付けるものとし、実際のフォーカスはコンテナーにあるため
:focusでは付けられない、としています。 - スクロール: APGは、アクティブな項目が変わるたびに画面内へスクロールするよう求めています。WAI-ARIAも、見える位置にあるようにすべき(SHOULD)としています。
- 項目の置き場所: WAI-ARIA 1.2は、参照する項目が、フォーカスのある要素のDOM上の子孫か、その
aria-ownsで指定した要素であることを求めています。例外は、フォーカスのある要素が、ポップアップをaria-controlsで指すcombobox、textbox、searchboxの場合で、項目はそのポップアップの中にあってもかまいません。
このプロパティを使えるのは、application、combobox、composite、group、textboxと、それを受け継ぐlistbox、grid、tree、tablist、toolbarなどのロールです。使う場面のタグ欄を、この方法で作ると次のようになります。
<label for="tags">タグ</label>
<input id="tags" type="text" role="combobox"
aria-autocomplete="list" aria-expanded="true"
aria-controls="tag-list" aria-activedescendant="tag-1">
<ul id="tag-list" role="listbox" aria-label="タグの候補">
<li id="tag-0" role="option" aria-selected="false">デザイン</li>
<li id="tag-1" role="option" aria-selected="true">デザインレビュー</li>
<li id="tag-2" role="option" aria-selected="false">デザイン案</li>
</ul>const input = document.getElementById('tags');
const options = [...document.querySelectorAll('#tag-list [role="option"]')];
function move(index) {
options.forEach((option, i) => option.setAttribute('aria-selected', String(i === index)));
input.setAttribute('aria-activedescendant', options[index].id);
options[index].scrollIntoView({ block: 'nearest' });
}/* フォーカスは入力欄に残るので、強調は:focusではなく状態から描きます。 */
#tag-list [aria-selected="true"] {
outline: 2px solid currentColor;
}APGのリストボックスのポップアップでは、選択がフォーカスに連動するので、アクティブな候補が選択中の候補にもなります。開閉、Enterキー、Escapeキー、候補の件数まで含めたスクリプトは、コンボボックスのガイドにあります。
05
どちらを使うか
APGは、複合ウィジェットの中のフォーカスを管理する方法として、両方を説明しています。どちらを選ぶかは、フォーカスをどこに置く必要があるかで決まります。
| ウィジェット | 使う方法 | 理由 |
|---|---|---|
| 編集できるコンボボックス(候補を出す入力欄) | aria-activedescendant |
リストが開いている間も入力を続けるので、DOMのフォーカスは入力欄に残す。APGのコンボボックスのパターンも、DOMのフォーカスはコンボボックスに保つとしている |
| 選択専用のコンボボックス(独自のドロップダウン) | aria-activedescendant。APGのコンボボックスのパターンがリストボックスのポップアップについて説明しているとおり |
矢印キーでリストを移動する間も、ボタン側がフォーカスを保つ。「キーボードで開けず、読み上げられないカスタムのドロップダウン」を参照 |
| タブ、ツールバー、ラジオグループ、ツリー、グリッド | どちらでもよい。ここで挙げたガイドはroving tabindexを使う | 実際のフォーカスなら、ブラウザーのフォーカスの枠とスクロールが使え、各項目のHTML標準のキー操作も残る |
次の場合は、方法が決まります。
- 項目自体が独自のキー操作を持つコントロールの場合: 座席表の各セルのボタンなどです。
aria-activedescendantでは、キーのイベントはコンテナーに届くため(WAI-ARIA 1.2の4.3節)、中のHTML標準のボタンがEnterキーやSpaceキーを自分で受け取ることはありません。roving tabindexなら、それぞれのキー操作が残ります。 - 必要に応じて描画する長いリスト: DOMにない項目にはフォーカスも参照もできません。どちらの方法でも、移る前にその項目を描画します。
06
スクリーンリーダーが読む内容と、画面に合わせたフォーカス
roving tabindexではフォーカスが実際に移るので、スクリーンリーダーは、新しくフォーカスを受けた項目の名前、ロール、状態をツリーから読み、製品ごとの言い方で伝えます。aria-activedescendant では、WAI-ARIA 1.2の4.3.2節により、ブラウザーは参照先の新しい要素でアクセシビリティAPIのフォーカスのイベントを発生させます。APGも、支援技術は実際にDOMのフォーカスが移ったときと同じフォーカス変更のイベントを受け取る、としています。仕様の上では、どちらの方法でも、スクリーンリーダーに伝わる内容は同じです。
違いが出るのは対応状況です。
- EaseWebの結果: 2026年11月2日の時点で、スクリーンリーダーの対応状況には、
aria-activedescendantについても、roving tabindexについても、まだ結果がありません。グリッドの中のボタンの間を矢印キーで移るテストも、結果はまだ記録されていません(各セルにボタンがあるグリッド)。 - 公開されている報告(EaseWebのテストではありません): 2023年11月に登録されたWebKitのバグ264891は、APG自身の例で、VoiceOverとSafariの組み合わせが、編集できるコンボボックスの
aria-activedescendantが指す候補を読まないと報告しています。報告者によれば、NVDAとJAWSは読んでいました。WebKitは2024年4月、同じ月に修正が取り込まれたバグ272565の重複としてこれを閉じ、確認には新しいVoiceOverが必要だと記しています。 - 読むためのモード: スクリーンリーダーは、ページを読む間、矢印キーを自分で使います。ウィジェットに矢印キーを渡すかどうかはロールとスクリーンリーダーによります。モードを切り替えるキーはスクリーンリーダーのショートカットにあります。
どちらの方法でも、フォーカスは画面に表示されている内容に合わせます。
- 強調とアクティブな項目は、常に同じ項目にします。 APGは、フォーカスの表示は常に見えていなければならず、選択中の状態はフォーカスの表示と見分けられなければならない、としています。
aria-activedescendantでは、ブラウザーの枠は入力欄にあるので、作成者が描く強調だけが現在位置の手がかりです。 - フォーカスを失わないようにします。 フォーカスのある項目を削除するときは、論理的に続く要素へフォーカスを移します(WAI-ARIA 1.2の4.3.1節)。そうしないと、フォーカスはbody要素へ移ってしまいます。
- 戻ってきたら同じ場所にします。 WAI-ARIAは、離れた後にまたフォーカスを受けたコンテナーでは、離れたときと同じ項目をアクティブにするよう勧めています。内容が変わった場合や、メニューバーのように最初の項目に戻ることが期待される場合は例外です。
- クリックとキーを一致させます。 クリックでも、キーと同じように
tabindexかaria-activedescendantを設定します。
07
実例
使う場面の2つの指摘は、次のように直しました。
- ツールバー:「元に戻す」以外のボタンに
tabindex="-1"が付いていました。ツールバーのパターンからマークアップだけを写し、スクリプトがなかったため、ほかの3つに届きませんでした。上のkeydownとfocusinの処理を加え、roving tabindexにしました。 - タグ欄: 下矢印キーでDOMのフォーカスをリストへ移していたため、次に入力した文字がどこにも入りませんでした。フォーカスは入力欄に残し、リストにコンボボックスのロールを付け、代わりに
aria-activedescendantを動かすようにしました。
もう一度キーボードで確かめた結果です。
| 確認 | 結果 |
|---|---|
| 上の部分からTabキーで進む | 「元に戻す」に移り、もう一度Tabでタグ欄へ進む |
| 右矢印キーを3回、さらに1回 | 「やり直す」「リンクを挿入」「画像を挿入」と進み、「元に戻す」に戻る |
| ツールバーでHomeとEnd | 「元に戻す」と「画像を挿入」 |
| 「リンクを挿入」をクリックしてTabで離れ、Shift + Tabで戻る | 「リンクを挿入」に戻る(クリックしたボタンにフォーカスを移すブラウザーの場合) |
| タグ欄に「デザ」と入力し、下矢印キー | フォーカスは入力欄のまま。「デザイン」が強調され、aria-activedescendant は tag-0 |
| リストが開いたまま「インレ」と続けて入力 | 文字は入力欄に入り、候補は「デザインレビュー」だけになる |
この表からは、各スクリーンリーダーが何を読むかは分かりません。サイトが対象にする組み合わせごとに確かめ、バージョンと日付とともに記録する必要があります。
08
よくある間違い
- すべての項目に
tabindex="0"を付ける。 Tabキーが項目ごとに止まり、グリッドやツリーに期待される矢印キーは効かないままです。「キーボードでセルを移動・編集できないデータグリッド」を参照してください。 tabindex="-1"のマークアップだけを写し、スクリプトを書かない。 1つ以外の項目に届かなくなり、すべてをTabの移動順に残すより悪くなります。「キーボードで押せないアイコンが並ぶツールバー」を参照してください。- コンボボックスのリストへDOMのフォーカスを移す。 次に入力した文字の行き場がなくなります。フォーカスは入力欄に残し、
aria-activedescendantを動かします。「候補を読み上げず、キーボードで選べない自動補完の入力欄」を参照してください。 aria-activedescendantで使う強調を、クラスや:focusで描く。:focusは項目には効かず、クラスでは属性と別の項目を強調してしまうことがあります。aria-selectedなどの属性から描きます。「キーボードで開けず、読み上げられないカスタムのドロップダウン」を参照してください。aria-activedescendantが、所有していない項目やページにない項目を指す。 WAI-ARIAは所有する要素を指すよう求めています。idがページのどの要素も指していなければ、ブラウザーはコンテナー自身でフォーカスのイベントを発生させます(4.3.2節)。- キー操作のないロール。 クリックできる行に
role="tree"とrole="treeitem"を付けても、「ツリー」と読まれるだけで、矢印キーは効きません。「キーボードで開けず、階層も伝わらないツリービュー」を参照してください。 - 切り替えに時間がかかるのに、選択をフォーカスに連動させる。 APGは、タブのパネルの表示に通信や再読み込みが必要な場合、矢印キーを押すたびに待たされると注意しています。その場合はEnterキーかSpaceキーで表示します。「マウスでしか切り替えられないタブ」を参照してください。
- クリックでTabの移動先が移らない。 Shift + Tabでウィジェットに戻ったとき、利用者が選んでいない項目に戻ります。「キーボードでもスクリーンリーダーでも選べない星評価」を参照してください。
09
規格の定め
- WCAG 2.2(要件):
- 達成基準2.1.1 キーボード(レベルA):利用者の動きの軌跡に依存する機能を除き、コンテンツのすべての機能を、個々のキー操作に特定のタイミングを求めずに、キーボードのインターフェイスで操作できること。
- 達成基準2.4.3 フォーカス順序(レベルA):順に移動できるWebページで、その順序が意味や操作に影響する場合、フォーカス可能なコンポーネントが、意味と操作性を保つ順序でフォーカスを受けること。
- 達成基準2.4.7 フォーカスの可視化(レベルAA):キーボードで操作できるユーザーインターフェイスに、フォーカスの表示が見える操作モードがあること。
- どの基準も、Tabの移動先を1つにすることや矢印キーを求めてはいません。すべてのボタンをTabの移動順に残したツールバーも2.1.1を満たすことは、ツールバーのガイドのとおりです。Tabの移動先を1つにするのはAPGの決まりです。EN 301 549 V3.2.1では9.2.1.1、9.2.4.3、9.2.4.7として、JIS X 8341-3:2016では2.1.1、2.4.3、2.4.7として同じ要件があります。Section 508(E205.4)はWCAG 2.0のレベルAとAAを求めているので、これらも含まれます。
- WAI-ARIA 1.2(W3C勧告、2023年6月6日)、ARIAを使うコンテンツへの要件:
- 4.3.1節:作成者は、grid、listbox、menu、menubar、radiogroup、tree、treegrid、tablistのコンテナーでフォーカスを管理しなければならない(MUST)。
aria-activedescendant:作成者は、参照する要素がフォーカスのある要素に所有されているか、aria-controlsを持つcombobox、textbox、searchboxの場合は、それが制御する要素に所有されていることを保証しなければならない(MUST)。また、アクティブな項目がフォーカスを受けたとき、見える位置にあるか、スクロールして表示されるようにすべき(SHOULD)としています。
- APG(助言): キーの割り当て、フォーカスが移る先、2つの方法は、推奨される実践で、要件ではありません。
10
制限
この記事が扱うのは、ウィジェットの中でフォーカスをどう動かすかです。次のことは扱いません。
- それぞれの方法で各スクリーンリーダーが何を読むか: 組み合わせごとに確かめるもので、結果は古くなります。スクリーンリーダーの対応状況を参照してください。
- キーボードでまったく届かないコントロール: クリックの処理しかない
divなどです。「Tabキーで到達できないdivのボタン」を参照してください。 - ロール、名前、状態: 「HTMLとARIAから、スクリーンリーダーに何が伝わるか」を参照してください。
- ページ全体で使うショートカット: APGでは別の節で扱っています。「オフにできない1文字のショートカット」を参照してください。
- タッチスクリーン: スマートフォンでスクリーンリーダーを使う人は、矢印キーではなくスワイプで移動します。そちらでもウィジェットを確かめてください。
ウィジェットのキー操作が利用者に合っているか、チームが使う部品のライブラリーが正しく実装しているかは、実際のページでテスト担当者が確かめます。
11
用語
- 複合ウィジェット
- フォーカス可能な部品をいくつも含み、Tabの移動先は1つのウィジェット。タブ、ツールバー、リストボックス、ラジオグループ、グリッド、ツリーなど。
- Tabの移動順
- TabキーとShift + Tabでフォーカスがたどる順路。
- roving tabindex
- ウィジェットの中で実際のフォーカスを動かす方法。アクティブな項目をtabindex="0"、ほかをtabindex="-1"にし、矢印キーで0とfocus()を移す。
- aria-activedescendant
- フォーカスのあるコンテナーや入力欄に付け、アクティブな項目のidを値にするARIAのプロパティ。支援技術はその項目にフォーカスがあるものとして扱う。
- DOMのフォーカス
- ブラウザーが持つフォーカス。document.activeElementが返す要素で、キーのイベントはここに届く。
- 選択がフォーカスに連動する
- 項目にフォーカスを移すと、その項目が選択もされること。矢印キーでタブに移るたびにそのパネルが表示されるタブなど。
12
出典とガイド
- 参考 ARIA Authoring Practices Guide: キーボード操作の開発:キーボード移動の基本的な決まり(新しいタブで開きます)
- 参考 ARIA Authoring Practices Guide: キーボード操作の開発:コンポーネントの中のキーボード移動(新しいタブで開きます)
- 参考 ARIA Authoring Practices Guide: roving tabindexによるコンポーネント内のフォーカス管理(新しいタブで開きます)
- 参考 ARIA Authoring Practices Guide: aria-activedescendantによる複合ウィジェット内のフォーカス管理(新しいタブで開きます)
- 参考 ARIA Authoring Practices Guide: コンボボックスのパターン:キーボード操作(新しいタブで開きます)
- 規定 WAI-ARIA 1.2: 4.3 フォーカスの管理とキーボード移動(新しいタブで開きます)
- 規定 WAI-ARIA 1.2: 6.7 aria-activedescendant(新しいタブで開きます)
- 規定 WCAG 2.2: 2.1.1 キーボード(新しいタブで開きます)
- 規定 WCAG 2.2: 2.4.3 フォーカス順序(新しいタブで開きます)
- 規定 WCAG 2.2: 2.4.7 フォーカスの可視化(新しいタブで開きます)
関連するガイド
- マウスでしか切り替えられないタブ 商品ページのタブをクリック処理付きのリスト項目で作っているため、キーボードでフォーカスできないうえ、どのパネルを表示しているかも伝わらない
- キーボードで押せないアイコンが並ぶツールバー 写真を編集するツールを、クリック処理を付けたspanのアイコンで作っているため、Tabキーはツールバー全体を飛ばし、スクリーンリーダーにはツールがあることさえ伝わらない
- キーボードで開けず、階層も伝わらないツリービュー セクションとページが並ぶサイドバーをクリックできるdivで作っているか、ツリーのロールを付けてもそのロールが約束するキー操作、状態、階層がないため、キーボードではセクションを開けず、スクリーンリーダーにはすべての項目が同じ階層に聞こえてしまう
- キーボードでもスクリーンリーダーでも選べない星評価 レビューのフォームで、クリックした星まで塗られる5つの星をクリックのハンドラー付きのspanか、display: noneで隠したラジオボタンで作っているため、Tabが星に届かず、スクリーンリーダーにも評価が伝わらないうえ、選んだ評価は塗られた星の見た目にしか表れない
- キーボードでセルを移動・編集できないデータグリッド 値を並べた表のセルはクリックしたときだけ入力欄に変わるため、role="grid" の有無にかかわらずキーボードではセルに届かず、クリックで開く入力欄にも名前がない
- 候補を読み上げず、キーボードで選べない自動補完の入力欄 入力欄の下に出る候補がクリック処理やCSSの強調表示だけの普通のリストのため、キーボードで選べず、スクリーンリーダーも候補が出たことを伝えない
- キーボードで開けず、読み上げられないカスタムのドロップダウン デザインに合わせてdivとリスト項目で作り直したドロップダウンは、クリックしか受け付けないか、クラスで描いた強調を動かすだけで、ラベルもどこからも指されないspanのため、キーボードでは選べず、スクリーンリーダーにはラベルも役割も値も選択肢も伝わらない
- キーボードで選べず、読み上げもされない座席表 座席がクリックにしか反応しない空のdivか名前のないボタンで、空席・選択中・予約済みの違いも色だけで示している
この記事は実務の進め方を、再現した例で説明するものです。スクリーンリーダーでのテスト結果ではなく、特定の顧客についての指摘でもありません。