Reading Order Bookmarklet

When a keyboard-only user or screen reader user comes to page that uses CSS to create a layout, there is a chance that what is on the screen does not match the flow of the page. In the posts HTML Source Order vs CSS Display Order and Source Order Matters I talk about the impact this can have on users when floats, absolute positioning, flex, and grid are used.

WCAG 2.1 Success Criterion 2.4.3: Focus Order addresses the need to keep in-page navigation sensible for user who are tabbing through the controls. I even wrote a bookmarklet to sequentially focus interactive controls to make testing a bit easier (if you like eating a sandwich with both hands for testing, for example).

However, non-interactive content on a page is not tabbed and so it can be harder for a tester to quickly identify when content does not flow in a sensible manner. Since a screen reader, for example, presents the page to a user based on the source order, a layout that visually shuffles the order can fail a WCAG review under 1.3.2: Meaningful Sequence and potentially 1.3.1: Info and Relationships.

In 2017 I proposed a feature for Firefox’s grid inspector as a blog post and filed a feature request against Firefox (Bug 1415410 – Add source order visualization to grid inspector). Last month I filed a request against Microsoft’s Accessibility Insights tool (Reading order visualization #394). I have also filed it as a feature request against The Paciello Group’s ARC (though in a private repo).

Until those features are built, and as a proof of concept, I have created a bookmarklet that can help:

Instructions

The bookmarklet will ask you for a selector and to choose between flex or grid (you can choose other display properties, but I did not test them). Make sure you are at the top of the page when you run the bookmarklet.

The function uses querySelectorAll(), so you can select a specific container on the page. This is probably a better approach than letting it parse the entire page, since generally you want to track reading order for a specific container.

A JavaScript prompt asking for a selector. A JavaScript prompt asking to choose flex or grid.
The two prompts from the script.
The entire previous caption and pair of images overlaid with the reading order numbers from the bookmarklet.
The previous set of images, which use flexbox for layout, when the bookmarklet is run.

Examples

The home page of this site after running the reading order bookmarklet to show the grid items.
An absurdly-wide view of my home page. I ran the bookmarklet without providing a selector, so it is showing all the grid containers and their grid items.
A grid-based character sheet showing just the outside grid container and its items.
My grid-based character sheet with just the page container and its grid items highlighted. For the selector prompt I entered “div”.
A grid-based character sheet showing one box on the page and its grid items.
The same character sheet, except for the selector I entered “#Stats dl” as that is the grid container for the stats block.

Caveats

This is not thoroughly tested. It is mostly a proof of concept. I make no guarantees nor warranties. Keep these things in mind when you run it:

The Script

Let’s walk through the script so you can get a sense of what I am doing and all the ways you can make it better.

This is where I prompt the user for a selector and then flex or grid. As you can see, it is not restricted to flex or grid, it just does not prove too valuable outside of those two.

function getNodes() {
  try {
    // Ask the user to choose nodes, display model
    var nodeSet = prompt("Selector? (blank searches most)","");
    var disProp = prompt("Flex or grid?","grid");

I write a style block to the top of the page. The numbered (semi-)circles are handled here. As you can see, I used some minified code I had lying around and adapted it. The styles use rems and system fonts so they hopefully do not get lost in the noise. I also have hover and focus styles because I added a tabindex to every <span>.

    // Add the styles
    var n,
      r = document.createElement("style");
    document.head.appendChild(r),
      (n = r.sheet).insertRule(
        "#ReadingOrderContainer span{position:absolute;border:.1rem solid #00f;border-radius:50%;background:#fff;color:#000;padding:.25rem .5rem;font-family:'Segoe UI',-apple-system,BlinkMacSystemFont,Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;text-align:center;min-width:1rem;line-height:1;box-shadow:.2rem .2rem .2rem rgba(0,0,0,.5);margin-left:-.5rem;margin-top:-.75rem}",
        0
      ),
      n.insertRule(
        "#ReadingOrderContainer span:focus,#ReadingOrderContainer span:hover{background-color:#00f;color:#fff;z-index:500;outline:0}",
        0
      );

If the user does not enter a selector then I use this collection of elements, which are generally not inline elements. Otherwise I use the selector that the user entered. As you can see, a CSS-style selector will work because I use querySelectorAll().

    // Get block-level plus some other nodes
    if (nodeSet == "") {
      var theNodes = document.querySelectorAll(
        "address, article, aside, blockquote, caption, details, dialog, dd, div, dl, dt, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, li, main, nav, ol, p, pre, section, table, td, th, ul, img, video, button, input, select, textarea"
      );
    } else {
      var theNodes = document.querySelectorAll(nodeSet);
    }

This function is a big loop-fest. This is mostly because my JavaScript skills are passable. I try to be a little efficient by setting up a couple arrays so I will not need to parse the entire DOM on each loop.

    // Create emtpy array for later population.
    var allNodesArray = [];
    var childNodesArray = [];

Now I loop through all the nodes, choosing only nodes whose display property matches what the user provided. I give those nodes a dotted red outline and feed them into my first array. Then I loop through the children of those nodes, ignore any that are set to display: none, give them a blue outline, and feed them into another array.

    // Populate array with only nodes of specific display type
    var a = 0;
    var b = 0;
    for (var i = 0; i < theNodes.length; i++) {
      if (
        window.getComputedStyle(theNodes[i]).getPropertyValue("display") ===
        disProp
      ) {
        allNodesArray[a] = theNodes[i];
        a = a + 1;
        theNodes[i].style.outline = ".4rem dotted rgba(255,0,0,.5)";
        for (var j = 0; j < theNodes[i].children.length; j++) {
          if (
            window
              .getComputedStyle(theNodes[i].children[j])
              .getPropertyValue("display") !== "none"
          ) {
            childNodesArray[b] = theNodes[i].children[j];
            b = b + 1;
            theNodes[i].children[j].style.outline =
              ".1rem solid rgba(0,0,255,.25)";
          }
        }
      }
    }

I don’t want to mess with the overall page by placing the numbers into the content, as that will break sibling selectors and similar in the site’s CSS. So I add a container at the end of the page to hold them all.

    // If there is already a counter box, remove it.
    var node = document.getElementById("ReadingOrderContainer");
    if (node) {
      if (node.parentNode) {
        node.parentNode.removeChild(node);
      }
    }
    // Define the container.
    var mainRegion = document.querySelector("body");
    var aside = document.createElement("aside");
    aside.id = "ReadingOrderContainer";
    // Insert the new elements.
    mainRegion.appendChild(aside);
    var LinkContainer = document.getElementById("ReadingOrderContainer");

Finally I loop through all the nodes in the child array (because I only care about the reading order of items in a container set to grid or flex), get the position of the top-left corner, and write them to new <span>s that contain the count. The styles I set above will ensure they are positioned absolutely.

    // Loop through the nodes
    var c = 1;
    for (var j = 0; j < childNodesArray.length; j++) {
      var childNode = childNodesArray[j];

      var rect = childNode.getBoundingClientRect();

      // Create the number container
      var noteNum = document.createElement("span");
      noteNum.setAttribute("class", "poscount");
      noteNum.setAttribute("tabindex", "0");
      noteNum.innerText = c;
      noteNum.style.left = rect.left + "px";
      noteNum.style.top = rect.top + "px";

      // Add it to the container
      LinkContainer.appendChild(noteNum);

      c = c + 1;
    }

That’s it. Gather any errors, write them to the console, and get out.

    // Done
  } catch (e) {
    console.log("getNodes(): " + e);
  }
}

I used a JavaScript minifier to squish the whole thing into a single line of CSS for the bookmarklet.

Update: Cycle Through the Counters

I took my Focus Interactive Controls bookmarklet and modified it to only cycle through span.poscount, making it a bit easier to demonstrate how the reading order can jump around the page.

Update: October 15, 2019

I made a pitch video:

If that video is too large or otherwise chunking, view it at YouTube, which does a better job with streaming. It has the same captions. Links to the screen shots and resources mentioned in the video are available at Smashing / Web We Want Video Pitch.

Update: September 9, 2020

The Source order viewer settings in the dev tools.
In Edge 86, press Ctrl + Shift + I to open the dev tools, then F1 to open the settings, arrow down twice to Experiments, then Tab to and select “Source order viewer”.

Microsoft has added a source order view to Edge 86 which, at the time of this writing, is in the Dev channel and must be enabled. It also lives in Chromium, so could do this in Chrome as well if you prefer. I give a bit more context and more screen shots in my post Source Order Viewer in Edge 86.

Give it a shot and see if it makes it easier when evaluating WCAG Success Criterion 1.3.2 Meaningful Sequence (A).

If you do not use a Chromium browser, the bookmarklet above that does the same thing (and was the basis of this browser feature). Firefox appears to still be working on the feature request as well.

No comments? Be the first!

Leave a Comment or Response

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>