CSSのreading-flowでTabの順番を見た目に合わせる方法
CSS の flex-direction: row-reverse や order、grid-template-areas で要素を並べ替えると、画面の見た目は変わっても、Tab キーで移る順番は HTML の順番のままです。親の要素に reading-flow を1行足すと、Tab の順番を見た目の並びに合わせられます。HTML を書き換える必要も、JavaScript も要りません。

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。ページを Playwright で開いて Tab キーを押し、フォーカスが移った要素を順に記録しました。アクセシビリティツリーは Chrome DevTools Protocol で取り出して調べました。Firefox や Safari での動き、スクリーンリーダーでの読み上げは試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。
見た目だけ並べ替えると、Tab の順番は HTML のまま
例として、ダイアログの下に並ぶ3つのボタンを考えます。HTML では大事な「保存」を先に書き、画面では右端に置きたいので、flex-direction: row-reverse で左右を入れ替えます。
<div class="bar">
<button>保存</button>
<button>下書き</button>
<button>キャンセル</button>
</div>
.bar {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
gap: 8px;
}
画面では、左から「キャンセル」「下書き」「保存」と並びます。ところが検証環境で Tab を押すと、フォーカスは「保存 → 下書き → キャンセル」の順に、右から左へ移りました。キーボードで操作する人には、フォーカスの枠が右端から逆向きに動いて見えます。
フレックスボックスでは flex-visual を指定する
.bar に reading-flow: flex-visual を足します。
.bar {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
gap: 8px;
reading-flow: flex-visual;
}
検証環境では、Tab の順番が「キャンセル → 下書き → 保存」になり、見た目どおり左から右へ移りました。Shift+Tab で戻る順番も「保存 → 下書き → キャンセル」で、ちょうど逆向きです。
フレックスボックスで使える値は、ほかに flex-flow があります。名前が似ていますが、MDN の説明によると、flex-visual は書字方向を考えたうえでの見た目の順番、flex-flow は flex-flow(flex-direction)の向きに従う順番です。上の例に flex-flow を指定すると、row-reverse の向きに沿って「保存 → 下書き → キャンセル」の順になりました。左から右へ読む日本語のページで見た目に合わせたいなら、flex-visual を選びます。
order で並べ替えた場合は、flex-visual と flex-flow のどちらでも、その並べ替えが反映されます。3つのリンクのうち3つ目に order: -1 を付けて「リンク3、リンク1、リンク2」と並べたところ、reading-flow なしでは HTML どおり「リンク1 → リンク2 → リンク3」、flex-visual と flex-flow ではどちらも見た目どおり「リンク3 → リンク1 → リンク2」でした。
グリッドでは grid-rows か grid-columns を指定する
grid-template-areas で置き場所を決めたグリッドでも、Tab の順番は HTML のままです。MDN の例と同じ、4つのリンクを並べたグリッドで試しました。
<div class="g">
<a class="a" href="#">A</a>
<a class="b" href="#">B</a>
<a class="c" href="#">C</a>
<a class="d" href="#">D</a>
</div>
.g {
display: grid;
grid-template-columns: repeat(3, 150px);
grid-template-areas:
"d b b"
"c c a";
gap: 8px;
}
.a { grid-area: a; }
.b { grid-area: b; }
.c { grid-area: c; }
.d { grid-area: d; }
.g a:first-child { order: 1; }
画面では、1行目に「D」と横長の「B」、2行目に横長の「C」と「A」が並びます。reading-flow の値ごとに、Tab の順番は次のようになりました。
reading-flow の値 |
Tab の順番 |
|---|---|
指定なし(normal) |
A → B → C → D |
grid-rows |
D → B → C → A |
grid-columns |
D → C → B → A |
grid-order |
B → C → D → A |
flex-visual |
A → B → C → D |
grid-rows は行ごとに左から、grid-columns は列ごとに上から進みます。grid-order は見た目の位置を見ず、HTML の順番に order だけを反映します。ここでは A に order: 1 が付いているので、A が最後に回りました。フレックスボックス用の flex-visual をグリッドに指定しても、順番は変わりませんでした。
画面幅でレイアウトが変わる場合
grid-rows は、MDN の説明にあるとおり、グリッドアイテムの見た目の順番に従います。そのため、メディアクエリーで並びが変わるページでも使えます。
.layout {
display: grid;
gap: 8px;
grid-template-areas: "main" "side";
reading-flow: grid-rows;
}
.main { grid-area: main; }
.side { grid-area: side; }
@media (min-width: 600px) {
.layout {
grid-template-columns: 200px 1fr;
grid-template-areas: "side main";
}
}
HTML では本文(<main>)を先、メニュー(<nav>)を後に書きました。検証環境では、画面幅 400px で上から本文、メニューの順に並び、Tab も「本文のリンク → メニューのリンク」でした。画面幅 800px ではメニューが左に来て、Tab も「メニューのリンク → 本文のリンク」に変わりました。
CSS の仕様には、HTML の順番は要素の論理的な順番を表すべきで、reading-flow はこの例のようにレイアウトによって読む順番が変わる場合のためにある、と書かれています。CSS が効かない状態でも意味が通るように、いちばん基本になる順番を HTML に書いておきます。
1つだけ順番を動かすなら reading-order
特定の要素だけを先や後ろに回したいときは、子要素に reading-order を指定します。値は整数で、指定しない要素は 0 です。小さい値の要素から順に移り、同じ値どうしは親の reading-flow の順番に従います。
親が普通のブロック要素で、並びを変えずに reading-order だけを使いたいときは、親に reading-flow: source-order を指定します。
.w {
reading-flow: source-order;
}
.w a {
display: block;
}
.w a:nth-child(3) {
reading-order: -1;
}
4つのリンクを縦に並べたところ、reading-order を付けない状態では HTML どおり「お知らせ1 → 2 → 3 → 4」でした。3つ目に reading-order: -1 を付けると「お知らせ3 → 1 → 2 → 4」に、1つ目に reading-order: 1 を付けると「お知らせ2 → 3 → 4 → 1」になりました。画面での並びは変わりません。
親の reading-flow が normal のままだと、reading-order: -1 を付けても順番は HTML どおりでした。reading-order は、親に normal 以外の reading-flow があるときだけ効きます。
tabindex とアクセシビリティツリーへの影響
reading-flow で並べ替わるのは、親の直接の子要素です。子要素の中に、さらにボタンが入っている場合も試しました。2つのカードを row-reverse で並べ、それぞれにボタンを2つ入れると、Tab は「カード2-a → カード2-b → カード1-a → カード1-b」の順でした。カードどうしは見た目の順に並べ替わり、カードの中のボタンは HTML の順番のまま、ひとまとまりで移ります。
ふだんはフォーカスされない <div> に tabindex="0" を付けた場合も、ボタンと同じように見た目の順番で移りました。tabindex のない <div> は、これまでどおり飛ばされます。
tabindex に正の値を付けた場合は、扱いが変わります。3つのボタンのうち「下書き」に tabindex="1" を付けて、ページを開いた直後から Tab を押していきました。reading-flow なしでは、「下書き」は「保存」と「キャンセル」の流れから外れました。フォーカスがページの外に出たあと、ページの中のほかのどの要素よりも先にフォーカスされました。reading-flow: flex-visual を指定した状態では、tabindex="1" がないときと同じ「キャンセル → 下書き → 保存」でした。MDN にも、正の tabindex で読みフローコンテナーの中の順番を変えようとしても無視される、と書かれています。
reading-flow は Tab の順番だけでなく、アクセシビリティツリーの並びも変えます。検証環境で Chrome DevTools Protocol からツリーを取り出すと、flex-visual を指定したときの3つのボタンは「キャンセル、下書き、保存」の順でした(指定なしでは「保存、下書き、キャンセル」)。仕様でも、読み上げの順番とフォーカスの順番の両方を決めるプロパティとされています。
対応しているブラウザー
互換性データ(@mdn/browser-compat-data 8.1.8)では、reading-flow と reading-order の対応状況は次のとおりです。どちらも実験的な機能とされています。
| ブラウザー | 対応したバージョン |
|---|---|
| Chrome、Edge、Android 版 Chrome | 137 |
| Opera | 121 |
| Samsung Internet | 30.0 |
| Firefox | 未対応 |
| Safari(iOS 版も) | 未対応 |
Firefox と Safari では、reading-flow に頼れません。「画面幅でレイアウトが変わる場合」で紹介した仕様の注意のとおり、HTML の順番そのものを論理的な順番にしておくことが前提になります。
JavaScript から対応しているかどうかを調べるには、CSS.supports() を使います。検証環境では、CSS.supports('reading-flow: flex-visual') と CSS.supports('reading-order: 1') はどちらも true、存在しない値の CSS.supports('reading-flow: visual') は false でした。





