Offscreen, Invisible and Size

This document explains how Chrome interprets the guidelines to apply the labels Offscreen and Invisible to nodes, and how the bounding box is calculated.

Background

In general, screen reading tools may be interested in all nodes regardless of whether they are presented to sighted users, but other Accessibility tools may care what is visible to sighted users.

Specifically, tools like Select-to-Speak and Switch Access should not look at nodes which are “offscreen”, “invisible”, or size=(0,0), as these are not visible on the screen for mouse interactions. On the other hand, ChromeVox and other screen readers may care about some of those nodes, which allow developers to insert buttons visible only to users with a screen reader, or to navigate below the fold.

Offscreen

In Chrome, we define Offscreen as:

Any object is offscreen if it is fully clipped or scrolled out of view by any of its ancestors so that it is not rendered on the screen.

For example, the staticText node here is offscreen:

<div style="width:0; height; 0; overflow: hidden">
  This text should be marked "offscreen", although its parent is not.
</div>

As background, MSDN defines Offscreen as an object is not rendered, but not because it was programmatically hidden:

The object is clipped or has scrolled out of view, but it is not programmatically hidden. If the user makes the viewport larger, more of the object will be visible on the computer screen.

In Chrome, we interpret this to mean that an object is fully clipped or scrolled out of view by its parent or ancestors. The main difference is that we are being explicit that any ancestor clipping a node can make it offscreen, not just a rootWebArea or scrollable ancestor.

Technical Implementation

Offscreen is calculated in AXTree::RelativeToTreeBounds. In this function, we walk up the accessibility tree adjusting a node‘s bounding box to the frame of its ancestors. If the box is clipped because it lies outside an ancestor’s bounds, and that ancestor clips its children (i.e. overflow:hidden, overflow:scroll, or it is a rootWebArea), offscreen is set to true.

Invisible

A node is marked Invisible in Chrome if it is hidden programmatically. In some cases, invisible nodes are simply excluded from the accessibility tree. Chrome defines invisible as:

Invisible means that a node or its ancestor is explicitly invisible.

This is the same as the definition from MSDN:

The object is programmatically hidden.

For example, these nodes are invisible:

<div style="display:none">
  This text should be marked 'invisible', along with its parent div.
</div>

<div style="visibility:hidden">
  This text should also be marked 'invisible' along with its parent div.
</div>

Technical implementation

See AXObject::IsVisible() (source).

Note: Opacity: 0 is explicitly not treated as invisible.

Bounding box calculation

A node's bounding box (location and size) are calculated based on its intrinsic width, height and location, and the sizes of its ancestors. We calculate size clipped by ancestors by default, but can also expose an unclipped size through the automation API.

The unclipped bounding box is helpful if you want to know the current coordinates of an element that's scrolled out of view, so you know how much to scroll to bring it in view.

The clipped bounding box is helpful if you want to draw a visible bounding box around the element on the screen. Clipping the bounding box helps sighted users understand what container the element is in, even if it's not currently visible. Without clipping you end up with elements floating outside of windows.

Technical implementation

A node‘s location and size are calculated inAXTree::RelativeToTreeBounds, and so closely tied to the offscreen calculation. In this function, we walk up the accessibility tree adjusting a node’s bounding box to the frame of its ancestors.

In general, this calculation is straight forward. But there are several edge cases:

  • If a node has no intrinsic size, its size will be taken from the union of its children.
    <!-- The outer div here has no size, so we use its child for its bounding box -->
    <div style="visibility:hidden" aria-hidden=false>
      <div style="visibility:visible">
        Visible text
      </div>
    </div>
  • If a node still has no size after that union, its bounds will be set to the size of the nearest ancestor which has size. However, this node will be marked offscreen, because it isn't visible to the user.

    • This is useful for exposing nodes to screen reader users.

In addition, AXTree::RelativeToTreeBounds is used to determine how location and size may be clipped by ancestors, allowing bounding boxes to reflect the location of a node clipped to its ancestors.

  • If a node is fully clipped by its ancestors such that the intersection of its bounds and an ancestor's bounds are size 0, it will be pushed to the nearest edge of the ancestor with width 1 or height 1.

    • We use width and height 1 instead of 0 to distinguish between empty or unknown sized nodes vs known small or clipped sized nodes.

Both clipped and unclipped location and size are exposed through the Chrome automation API.

  • location is the global location of a node as clipped by its ancestors. If a node is fully scrolled off a rootWebArea in X, for example, its location will be the height of the rootWebArea, and its height will be 1. The Y position and width will be unchanged.

  • unclippedLocation is the global location of a node ignoring any clipping by ancestors. If a node is fully scrolled off a rootWebArea in X, for example, its location will simply be larger than the height of the rootWebArea, and its size will also be unchanged.