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:
Copy or drag the link to your bookmarks: Reading Order
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.
Examples
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:
- Run it while the page is scrolled to the top, otherwise the counters may not be anywhere near the containers.
- If your containers already have outline styles, this script will override them.
- The outline styles are low contrast and may get lost in the noise, and they are based on
rem
, so they will not scale with the text. - When prompted for a selector, leaving it blank will run the script against my arbitrary selection of nodes, which you can see below.
- It will number parents before it numbers children, as the reading order is only for the context of a single grid or flex container. That means nested grid or flex areas may be numbered in a way that implies they come well after the containers, which is not accurate.
- If you want to Tab through the numbers, note that they do not start until after all the other tabbable controls on the page.
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 rem
s 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.
Copy or drag the link to your bookmarks, making sure to use it only after running the bookmarklet at the top of this post: Focus Reading Order Counters
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
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.
Leave a Comment or Response