タブ順序のウォークスルー

ウェブページでTabを押すと、フォーカスは次のリンク、ボタン、入力欄へ移ります。このチェックは任意のアドレスについてその道筋をすべて書き出し、道筋から外れたコントロールを指摘します。

ページを1回読み、同じサイトのスタイルシートを最大5つ読みます。何も保存しません。

例: mozilla.org, wikipedia.org

フォーカスはキーボードのカーソル

マウスはどこでも指せます。キーボードは一度に1つの要素の「上」にしかいられず、その位置をフォーカスと呼びます。Tabで前へ、Shift+Tabで後ろへ移り、Enterでリンクをたどり、Spaceでボタンを押します。手の震えや動きの制限がある人はこうして操作します。スクリーンリーダーの利用者、スイッチ装置や音声操作を使う人、フォームを素早く入力したい人も同じです。

何がフォーカスを受けられるかは、ブラウザがHTMLだけから判断します。href付きのリンク、ボタン、フォームフィールド、<summary>、コントロール付きのメディアプレーヤー、フレームは、ソースに現れる順で最初から道筋に入っています。それ以外は、tabindex属性で指定しない限り飛ばされます。

tabindexの3つの意味

値効果使う場面
tabindex="0"要素を本来の位置で道筋に加えるネイティブの<button>にできない独自コントロール
tabindex="-1"道筋から外す。スクリプトからはフォーカスできる非アクティブなタブ、矢印キーで移るメニュー項目、画面外のカルーセルのスライド
tabindex="1"以上ほかのすべてより先に、小さい数から順に移すほぼ使わない。フォーカスが画面中を飛び回る

このウォークスルーはこのルールに厳密に従います。まず正の値を小さい順に、次にソースの順です。同じnameのラジオボタンのグループは、矢印キーでグループ内を移動するので1つの停止位置と数えます。無効なコントロール、閉じた<dialog>要素、inertやhiddenの領域、閉じた<details>の中身は除外します。

チェックが指摘するもの

キーボードで届かないクリック要素
<div onclick>や、role="button"があってtabindexのない要素です。マウスなら押せますが、キーボードではたどり着くことさえできません。<button type="button">に置き換えれば、フォーカス、Enter、Spaceが何もしなくても手に入ります。
hrefのないリンク
行き先のない<a>は、ブラウザにとってただのテキストで、Tabはそのまま通り過ぎます。
tabindex="-1"のコントロール
同じアドレスへの別の到達可能なリンクと重複している場合や、矢印キーが操作を引き継ぐタブリスト、メニュー、ツールバーなどのグループに属する場合を除き、報告します。
スキップリンク
最初の3つの停止位置に、<a href="#content">Skip to content</a>のような同じページ内のアンカーへのリンクがあるかを探し、対象のidが存在することを確かめます。また、ページの先頭から<main>までに何回押す必要があるかも数えます。5回以下なら、スキップリンクがないことは参考として記すだけです。
フォーカス枠
ページの<style>ブロック、インラインスタイル、同じサイトの最大5つのスタイルシートから、a:focus { outline: none }や* { outline: 0 }のようなルールを探します。:focus-visibleや:focusのスタイルで枠の代わりを用意していなければ、結果は赤です。キーボード利用者が自分の位置を見られないからです。
ほかに見ておきたい停止位置
aria-hidden="true"の中にあるフォーカス可能な要素、アクセシブルな名前のない停止位置、titleのないフレーム、ボタンとして使われている#やjavascript:へのリンクです。

見えるフォーカスを取り戻す

デザイナーが枠を消すのは、マウスでクリックしたあとに表示されて見た目が悪いと感じるからであることが多いです。:focus-visibleセレクターがこの問題を解決します。ブラウザが「利用者にフォーカスを見せる必要がある」と判断したときだけ一致し、実際にはキーボードで操作しているときを意味します。

:focus-visible {
  outline: 3px solid #1a5fb4;
  outline-offset: 2px;
}

WCAG 2.2は、周囲の色に対して3:1以上のコントラストを持つインジケーターを求めています。:focus:not(:focus-visible) { outline: none }というパターンは安全で、報告されません。

静的な読み取りの限界

道筋は、こちらのサーバーが受け取ったHTMLから計算します。JavaScriptのaddEventListenerで付けたクリックハンドラーはマークアップに跡を残さないので、そうして作られた<div>は検出されません。スクリプトで組み立てるメニューもここでは見えず、フォーカスがウィジェットに入ったまま出られなくなるキーボードトラップも同様です。別ドメインのスタイルシートや、6つ目以降のスタイルシートは読みません。

最後は実際のテストで締めくくってください。2分で済みます。マウスを脇に置き、ページを読み込み、フッターに着くまでTabを押します。自分の位置が常に見え、すべてのメニューを開き、すべてのフォームを送信できるはずです。

よくある質問

キーボード操作を自分でテストするには?

アドレスバーをクリックしてから、Tabを何度も押します。Shift+Tabで戻り、Enterはリンク、Spaceはボタンとチェックボックス、矢印キーはラジオグループやメニューの中で使い、Escapeでダイアログを閉じます。macOSのSafariでは、先に詳細設定で「Tabキーを押したときにWebページ上の各項目を強調表示」をオンにしてください。

divにtabindex="0"を付ければボタンになりますか?

いいえ。フォーカスできるようにはなりますが、キーハンドラーを追加するまでEnterもSpaceも何もしません。またrole="button"がなければ、スクリーンリーダーはそれをボタンと読み上げません。ネイティブの<button>ならこの3つがすべてそろいます。

main要素があればスキップリンクは要りませんか?

要ります。画面を見ながらキーボードで操作する人のためです。スクリーンリーダーはショートカットで<main>ランドマークに飛べます。Tabキーだけを使う人にはそのようなショートカットがなく、スキップリンクがなければメニュー項目をすべて通らなければなりません。

Tabの順序が画面の見た目と合わないのはなぜですか?

TabはHTMLソースの順に従い、見た目のレイアウトには従いません。order、flex-direction: row-reverse、グリッド配置、絶対配置などのCSSは、ソース内の位置を変えずに画面上の要素を動かせます。正のtabindexも同じ効果を持ちます。

outline: noneは常にアクセシビリティ違反ですか?

代わりが何もない場合だけです。同じブロックで枠を消し、フォーカス時に見えるボーダー、背景、ボックスシャドウを設定しているルールは問題なく、このチェックも代わりがあると数えます。