← Read

Tech · · 6 min read

When the tab order stops matching what you see

reading-flow tells the browser to follow the visual order for focus and screen readers instead of the DOM. The seven values, and the thing it deliberately does not fix.

Move three flex items around with order, or place grid items into named areas, and the page looks right while the keyboard walks through it in a different sequence. reading-flow closes that gap:

.toolbar {
  display: flex;
  flex-direction: row-reverse;
  reading-flow: flex-visual;
}

With that declaration, sequential navigation and screen-reader order follow what a sighted reader sees — left to right in a left-to-right document — instead of the DOM.

It ships in Chromium only. At the time of writing, MDN's compatibility data records reading-flow and reading-order in Chrome 137 and later, with false for Firefox and Safari, so this is not yet a fix you can rely on everywhere. It is also not the fix for most source-order problems, which is the part worth reading before you use it.

Why order created this problem

CSS has been explicit about this for years. From CSS Display Module Level 4: the order property "does not affect the default traversal order of sequential navigation modes", and, as an advisement, "Authors must use order only for spatial, not logical, reordering of content. Style sheets that use order to perform logical reordering are non-conforming."

That rule exists so that non-visual presentations can trust the source order. It also leaves a genuine gap: a component whose visual arrangement legitimately differs per breakpoint has only one DOM order to offer, and one of those layouts will read wrong. reading-flow and reading-order exist for that case.

The seven values

drafts.csswg.org ↗CSS Display Module Level 4: the reading-flow propertyThe normative definition, including every value and the advisement about source order. Source: CSS Working Group

reading-flow applies to block, flex and grid containers. Any value other than normal makes the element a reading flow container.

ValueTakes effect onWhat the order becomes
normal—DOM order. The initial value.
source-orderblock, flex, gridDOM order, but reading-order on the children is honoured
flex-visualflex onlyThe visual order of the flex items, accounting for writing mode
flex-flowflex onlyThe flex-flow direction
grid-rowsgrid onlyVisual order of grid items, row by row
grid-columnsgrid onlyVisual order of grid items, column by column
grid-ordergrid onlyOrder-modified document order — identical to normal unless order is used

Two consequences of the "takes effect on" column are easy to miss. flex-visual on a grid container does nothing at all — it is not an error, it simply has no effect. And when you use any flex-* or grid-* value, the order property is taken into account, which is usually the whole point.

Here is the spec's grid example. Four items are placed by area so that they appear in the order 4, 2, 3, 1:

<div class="wrapper">
  <a class="a" href="#">Item 1</a>
  <a class="b" href="#">Item 2</a>
  <a class="c" href="#">Item 3</a>
  <a class="d" href="#">Item 4</a>
</div>
.wrapper {
  display: grid;
  grid-template-columns: repeat(3, 150px);
  grid-template-areas: "d b b"
                       "c c a";
  reading-flow: grid-rows;
}

.a { grid-area: a; }
.b { grid-area: b; }
.c { grid-area: c; }
.d { grid-area: d; }

The reading order becomes "Item 4", "Item 2", "Item 3", "Item 1" — the order they appear on screen.

reading-order, for the one item that is out of place

Whole-container keywords do not cover every case. reading-order takes an <integer> on a direct child of a reading flow container and moves that item within the flow:

.wrapper { reading-flow: grid-rows; }
.top     { reading-order: -1; }

Lower ordinal groups are visited first, and the initial value is 0, so -1 pulls an item to the front without touching anything else. Where two items land in the same ordinal group, reading-flow breaks the tie.

This is also the reason source-order exists as a value. On its own it changes nothing about the order — it exists to turn the parent into a reading flow container so that reading-order on the children starts working:

.wrapper           { reading-flow: source-order; }
.wrapper a:nth-child(3) { reading-order: -1; }

What it deliberately does not do

Three limits, all in the spec.

It does not move anything. reading-flow "affects neither layout nor painting order and therefore has no effect on rendering to the visual canvas". It changes focus order and what is read aloud. Nothing else.

It is not a licence to write your markup in any order. The spec's advisement is blunt: "The source document should express the underlying logical order of elements." reading-flow and reading-order are "for cases where a given document can have multiple reading orders depending on layout changes, e.g. in response to media queries", and even then, "the most common or most fundamental reading order should be encoded in the source order so that the document is sensical without CSS". If your markup is scrambled at every breakpoint, the fix is the markup.

It only reaches direct children. reading-order applies to direct block-level, grid item or flex item children of a reading flow container. Nested containers need their own declaration.

It is also worth knowing that the spec has an open issue about whether the property should apply to tables, and the design notes record the principle the whole feature is built on: "Linear navigation, focus sequencing order, and screen-reader order should always match, because there are users who use them together."

Where this bites in real layouts

Auto-placed layouts create this mismatch without anyone writing order at all. In CSS grid lanes — the masonry layout mode — a spanning item that does not fit can land lower than items that come after it in the DOM, so the visual and DOM orders diverge purely from the algorithm. The Grid Level 3 spec points at reading-flow as the remedy, with the caveat that it is only appropriate "if the items do not have an inherent order".

That caveat is the decision rule. Search results, a photo wall, a tag cloud: no inherent order, so re-ordering for reading is honest. A numbered set of steps, a leaderboard, a chronological feed: the order carries meaning, so fix the source instead.

Shipping it today

Because it changes nothing visual, reading-flow degrades to exactly the situation you already had: engines that do not support it keep the DOM order. That makes it safe to add now and get the benefit for Chromium users, as long as you have not used it to justify markup that reads badly elsewhere. Wrap it in @supports (reading-flow: grid-rows) only if you want to change other properties alongside it.

Support here is time-sensitive. Check it again before treating it as the accessibility story for a layout.

FAQ

Does reading-flow fix the order for screen readers as well as the Tab key?

That is its stated purpose: it "controls the order in which elements are rendered to speech or are navigated to when using (linear) sequential navigation methods". Both should move together.

Can I use it instead of tabindex?

They solve different problems. A positive tabindex moves an element within the focus order of the whole page, which is hard to keep correct as a page grows; reading-flow reorders within one container and leaves everything outside it alone.

Does reading-flow: grid-rows work on a flex container?

No. Each keyword names the layout mode it takes effect on. A grid-* value on a flex container does nothing.

Is it Baseline?

No. At the time of writing it is Chromium-only, with no support in Firefox or Safari.

What happens to items I have not given a reading-order?

They stay in ordinal group 0, the initial value, and are ordered among themselves by whatever reading-flow value the container has.

Sources

More to read