本文へ移動
easeweb
ご相談

ウィジェットの中の矢印キー: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の手順は次のとおりです。

  1. ウィジェットを作るとき、最初の項目に tabindex="0"、ほかの項目に tabindex="-1" を付けます。
  2. 矢印キーが押されたら、0 を持つ項目を tabindex="-1" にし、フォーカスを移す先の項目を tabindex="0" にします。
  3. その項目で 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つの指摘は、次のように直しました。

  1. ツールバー:「元に戻す」以外のボタンに tabindex="-1" が付いていました。ツールバーのパターンからマークアップだけを写し、スクリプトがなかったため、ほかの3つに届きませんでした。上のkeydownとfocusinの処理を加え、roving tabindexにしました。
  2. タグ欄: 下矢印キーで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

制限

この記事が扱うのは、ウィジェットの中でフォーカスをどう動かすかです。次のことは扱いません。

ウィジェットのキー操作が利用者に合っているか、チームが使う部品のライブラリーが正しく実装しているかは、実際のページでテスト担当者が確かめます。

11

用語

複合ウィジェット
フォーカス可能な部品をいくつも含み、Tabの移動先は1つのウィジェット。タブ、ツールバー、リストボックス、ラジオグループ、グリッド、ツリーなど。
Tabの移動順
TabキーとShift + Tabでフォーカスがたどる順路。
roving tabindex
ウィジェットの中で実際のフォーカスを動かす方法。アクティブな項目をtabindex="0"、ほかをtabindex="-1"にし、矢印キーで0とfocus()を移す。
aria-activedescendant
フォーカスのあるコンテナーや入力欄に付け、アクティブな項目のidを値にするARIAのプロパティ。支援技術はその項目にフォーカスがあるものとして扱う。
DOMのフォーカス
ブラウザーが持つフォーカス。document.activeElementが返す要素で、キーのイベントはここに届く。
選択がフォーカスに連動する
項目にフォーカスを移すと、その項目が選択もされること。矢印キーでタブに移るたびにそのパネルが表示されるタブなど。

12

出典とガイド

関連するガイド

この記事は実務の進め方を、再現した例で説明するものです。スクリーンリーダーでのテスト結果ではなく、特定の顧客についての指摘でもありません。