/reading-order-items

Explainer for new CSS property reading-order-items

CSS reading-order-items Explainer

Author: Di Zhang

Last updated: June 5, 2024

Issue: TBD

Introduction to the problem

Focus navigation is the mechanism that allows users to navigate and access the contents of a website using their keyboard. Currently, this navigation follows the source order aka the order the elements are defined in the DOM tree. This causes a disconnect when the elements are displayed in a different order, using a flexbox or grid layout, where the visual reading order can be different to the underlying source order using features like the order property.

The CSS Working Group proposed to solve this problem using the new CSS property reading-order-items. This property allows users to choose how items within a flex or grid container should be read. In this explainer, we are proposing changes to the WHATWG specifications to support this new property for sequential focus navigation. Namely, we propose adding a new focus scope owner and more steps to the sequential navigation search algorithm.

Note this feature will become even more valuable in the upcoming CSS Masonry, which uses an automatic layout method in which items are displayed in a hard-to-predict order.

New specifications in WHATWG

Definitions

A reading order container is either

  • a flex container that has the CSS property reading-order-items set to flex-visual or flex-flow.
  • a grid container that has the CSS property reading-order-items set to grid-rows, grid-columns or grid-order.

A reading order item is a flex item or grid item whose layout parent is a reading order container.

New Focus Navigation Scope Owner

The definition of focus navigation scope owner should be modified:

A node is a focus navigation scope owner if it is a Document, a shadow host, a slot, an element in the popover showing state which also has a popover invoker set, or a reading order container.

Add this to the associated focus navigation owner algorithm, after existing step 2 and before the existing step 3:

3. If element’s layout parent is a reading order container, then return the reading order container element.

Changes to sequential navigation search algorithm

https://html.spec.whatwg.org/multipage/interaction.html#sequential-navigation-search-algorithm

Add new steps after existing step 1 and before the existing step 2:

1.5. If candidate is a reading order item or null, direction is "forward" and starting point is in a reading-ordered focus navigation scope scope, then let new candidate be the result of the reading order sequential navigation search algorithm with candidate, direction and scope.

If starting point is a reading order item, direction is "backward" and starting point is in a reading-ordered focus navigation scope scope, then let the new candidate be the result of the reading order sequential navigation search algorithm with starting point, direction and starting point’s focus navigation scope.

If new candidate is null, then let starting point be candidate, and return to step 1 of this algorithm. Otherwise, let candidate be new candidate.

reading order sequential navigation search algorithm

To find the next item in reading order, given a reading order item current, a direction direction and a reading ordered focus navigation scope scope, perform the following steps. They return an element.

  1. Let reading order items be the list of reading order items owned by scope, sorted in reading order.
  2. If reading order items is empty, return null.
  3. If direction is “forward”, then:
    1. Let previous be the reading order item that comes before current, in DOM tree order.
    2. If previous is null, return the first item in reading order items.
    3. Otherwise, if previous is the last item in readinging order items, return null.
    4. Otherwise, return the item that comes after previous in _reading order items.
  4. Otherwise:
    1. Let previous be the item that comes before current in reading order items.
    2. If previous is null, return null.
    3. Otherwise, if previous does not have any DOM tree descendants, return previous.
    4. Otherwise, return the last DOM tree descendant of previous.

Changes to tabindex-ordered focus navigation scope

https://html.spec.whatwg.org/multipage/interaction.html#tabindex-ordered-focus-navigation-scope

Change

The order within a tabindex-ordered focus navigation scope is determined by each element's tabindex value, as described in the section below.

to

The order within a tabindex-ordered focus navigation scope is determined by each element's tabindex value and, for reading-ordered focus navigation scopes, by the special rules provided by the sequential navigation search algorithm. Note tabindex takes precedence over reading order.

Add new section 6.6.N The Reading Order

A reading-ordered focus navigation scope is a tabindex-ordered focus navigation scope where the scope owner is a reading order container.

The reading order for a reading-ordered focus navigation scope is determined by the container’s reading-order-items value.

If the value is flex-visual,

  • The reading order should be defined by the flex items, sorted in the visual reading order and taking the writing mode into account.

If the value is flex-flow

  • The reading order should be defined by the flex items, sorted by the CSS ‘flex-flow’ direction.

If the value is grid-rows,

  • The reading order should be defined by the grid items, sorted first by their displayed row order, and then by their column order, taking the writing mode into account.

If the value is grid-columns,

  • The reading order should be defined by the grid items, sorted first by their displayed column order, and then by their row order, taking the writing mode into account.

If the value is grid-order,

Examples

Example - grid-order

<!DOCTYPE html>
<style>
.wrapper {
  display: grid;
  reading-order-items: grid-order;
}
</style>
<div class="wrapper">
 <button id="a" style="order: 2">A</button>
 <button id="b" style="order: 4">B</button>
 <button id="c" style="order: 3">C</button>
 <button id="d" style="order: 1">D</button>
</div>

Follows the order-modified document order, unless the order property has been used to change the order of items.

Forward navigation

Current is null or outside the scope

  1. Find first element in scope.
  2. Find first element in Reading order.
  3. Return D

Current is D

  1. Find next DOM from D: null since outside the scope.
  2. Find previous. Since next DOM is null, this is last element D.
  3. Move forward from D in reading order.
  4. Return A.

Current is A → Return C

Current is C → Return B

Current is B

  1. Find next DOM from B, aka C.
  2. Find previous from C, aka B.
  3. Move forward from B in reading order.
  4. Return null.

Backward navigation

Current is null or outside the scope

  1. Find last element in scope.
  2. Find last element in Reading order.
  3. Return B

Current is B

  1. B is a reading order item.
  2. previous is C.
  3. Iterate in DOM tree order until next reading item after C, aka D.
  4. Return DOM element before D, aka C.

Current is C → Return A

Current is A → Return D

Current is D

  1. D is a reading order item.
  2. previous is null.
  3. Return null as we are already at last item to visit.

Example - grid-order with nested children

<!DOCTYPE html>
<style>
.wrapper {
 display: grid;
 reading-order-items: grid-order;
}
</style>
<div class="wrapper">
 <div id="A" style="order: 2">A
   <button id="a" style="order: 3">Button A</button>
 </div>
 <div id="B" style="order: 3">B
   <button id="b">Button B</button>
 </div>
 <div id="C" style="order: 1">C
   <button id="c">Button C</button>
 </div>
</div>

Forward navigation

Current is null or outside the scope

  1. Find first element in scope.
  2. Find first element in Reading order.
  3. Return C

Current is C

  1. Find next DOM from C, aka c.
  2. Since c is not a reading order item nor is it null, return c.

Current is button c

  1. Find next DOM from c, aka null.
  2. Find previous. Since next DOM is null, this is C.
  3. Move forward from C in reading order.
  4. Return A.

Current A → Return a.

Current a → Return B.

Current B → Return b.

Current is b

  1. Find next DOM from b, aka null.
  2. Find previous. Since next DOM is null, this is B.
  3. Move forward from B in reading order.
  4. Return null.

Backward navigation

Current is null or outside the scope

  1. Find last element in scope.
  2. Find last element in Reading order, B
  3. Find last child element within it, return b

Current is b

  1. b is not a reading order item
  2. Reading the previous DOM order element, B

Current is B

  1. B is a reading order item.
  2. previous is A.
  3. Iterate in DOM tree order until next reading item after A, aka B.
  4. Return DOM element before B, aka a.

Current is a → Return A

Current is A → Return c

Current is c → Return C

Current is C

  1. C is a reading order item.
  2. previous is null.
  3. Return null as we are already at last item to visit.

Open Questions

What should be the reading order if reading order items are defined through display: contents and cross different scopes?

<!DOCTYPE html>
<meta charset="utf-8">

<div>
<template shadowrootmode="open" shadowrootdelegatesfocus>
<style>
.wrapper {
  display: flex;
  reading-order-items: flex-visual;
}
</style>
<div class=wrapper>
<button id="A" style="order: 2">Item A</button>
<slot></slot>
<button id="C" style="order: 4">Item C</button>
</div>
</template>

<button id="B1" style="order: 1">Slotted B1</button>
<button id="B2" style="order: 3">Slotted B2</button>
</div>

Render:

Source order: A,B1,B2,C

Reading order: B1,A,B2,C

Given the flattened tabindex-ordered focus navigation scope, step 2.2, we should visit all elements within a scope together (so B1, then B2). However, that is visually the wrong order.

What should be the reading order if a reading order item is a display: contents scope owner?

<!DOCTYPE html>
<meta charset="utf-8">

<style>
 .wrapper {
   display: flex;
   reading-order-items: flex-visual;
 }
 </style>
<div class=wrapper id="root">
 <div style="display: contents">
   <template shadowrootmode=open>
     <slot></slot>
   </template>
   <button id="A2" style="order: 2">A</button>
   <button id="B2" style="order: 1">B</button>
 </div>
 <button id="C" style="order: 3">C</button>
</div>

Render:

Source order: A,B,C

Reading order: B,A,C

In this case, we have a DIV that is:

  • A Shadow host (so a focus navigation scope owner)
  • Its layout parent is a reading order container
  • Has display: contents

Should the DIV qualify as a reading order item? If so, it can be included in the defined **reading-ordered focus navigation scope, **but there isn’t a straightforward way to include it in the reading order, since it isn’t part of the reading order container, and isn’t displayed on its own. So it’s unclear where it belongs with respect to the other reading order items.

List of relevant issues

csswg-drafts issue 9230 Define how reading-order / reading-order-items interact with focusable display: contents elements.

csswg-drafts issue 7387 Providing authors with a method of opting into following the visual order, rather than logical order

csswg-drafts issue 9921 Is reading-order-items the best name for this property?

csswg-drafts issue 9922 Should the reading-order-items property apply to tables in addition to flex and grid layouts?

csswg-drafts issue 9923 Proposed alternative syntax for reading order

csswg-drafts issue 8589 Do we need reading-order: <integer> or should reading-order: auto be allowable in all grid or flex layouts?

csswg-drafts issue 8257 Define 'reading-order: auto'