When CSS Silently Rewrites the Accessibility Tree

CSS is commonly viewed as the presentational layer of the web—the part that colors, spaces, and styles but doesn't touch meaning. In her CSS Day 2024 presentation, Sara Soueidan laid out the many ways that assumption falls apart: CSS changes not only what we see, but what assistive technology announces, how elements are named, and whether they appear in the accessibility tree (accTree) at all.

Each object in the accTree carries four key pieces of information. The role describes what kind of thing the element is. The name identifies the element in the interface. The description provides further context. And the state communicates what the element is currently doing. Objects can also carry properties and relationships, such as membership in a group or a labeling relationship with another element.

Because CSS can influence any of these, styling decisions have direct consequences for what screen reader users hear—or don't hear.

Danger Zones: display and List Semantics

One of the most common styling choices that affects the accTree is removing list markers. While the source's example is specific to Safari's handling of lists without markers, the general principle applies broadly: a visual change to a list can affect how its semantics are announced.

/* Removes list role semantics in Safari */
/* Need to add aria-role=list */
ul {
  list-style: none;
}

/* Does not remove role semantics in Safari */
nav ul {
  list-style: none:
}

/* Removed unless specifically re-added in the markup */
ul:where([role="list"]) {
  list-style: none;
}

/* Preserves list semantics */
ul {
  list-style: "";
}

A more aggressive case is display: contents. This value generates no box for the element itself, but unlike display: none, it leaves the children untouched and rendered. The element disappears from the accessibility tree while its children are bumped up a level in the DOM.

Exposed to a11y APIs?Keyboard accessible?Visually accessible (rendered)?Children exposed to a11y APIs?
display: none
visibility: hidden
opacity: 0 and filter: opacity(0)
clip-path: inset(100%)
position(off-canvas)
.visually-hidden
display: contents

The implications become severe when display: contents is applied to semantically meaningful or interactive elements: the element is removed from the accTree completely, and anything dependent on it for naming or relationships breaks. There is also an open bug in Firefox where the value breaks the anchoring effect of an ID attribute on the element. For safe usage, apply it to generic <div>s only.

Styling Hidden Text the Right Way

Utility classes for visually hiding text while keeping it available to screen readers are widespread, but they need care. A robust implementation includes :not() clauses that exempt focused or activated elements from the hiding rule, so an interactive element that receives keyboard focus is not left invisible.

.visually-hidden:not(:focus):not(:active) {
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0); /* for IE only */
  clip-path: inset(50%);
  position: absolute;
  white-space: nowrap;
}

Those :not() statements are critical. If you hide an interactive control such as a checkbox with a visually hidden utility and replace its appearance with a custom one, pushing the native control off-screen entirely will cause assistive technology to lose track of it. The control is hidden from the accTree, so it is neither announced on focus nor on activation. Positioning the native control directly over the custom visual—using absolute positioning—preserves the interactive accessibility while letting CSS style the presentation. The bottom line: before using a visually hidden utility on an interactive element, ask yourself whether it will be visible again when it receives focus.

How the Accessible Name Is Computed

The browser determines an element's accessible name through a specific sequence. First it checks for aria-labelledby and, if the referenced element resolves, uses its computed text. Otherwise it checks for aria-label. If neither is present and the element does not carry role="presentation" or role="none", the browser looks for an HTML source: an alt or title attribute (preferably on an <iframe>), a labeling element such as <label> or <legend>, or the element's own contents.

Since ARIA takes precedence over HTML in this computation, it is easy to accidentally override a native accessible name. The order in the algorithm is not, however, the order of preference for authors—prioritize native HTML over ARIA. DevTools can show you the priorities and overrides for any element under the Accessibility tab.

DevTools exposing the accessibility tree of the document and aria attributes for a selected anchor element.

The Trouble with CSS Generated Content

Using CSS pseudo-elements to inject meaningful content introduces several problems:

<a href="#" class="info">CSS generated content</a>
.info::before {
  content: "ⓘ" / "Info: ";
  /* or */
  content: url('path-to-icon.svg') / "Info: ";
}

/* Contents: : Info: CSS generated content. */

Generated content cannot be translated by automated tools, and it disappears entirely when CSS is stripped away, as happens in contexts like Safari's Reader Mode. There are further edge cases: in Forced Colors environments the content may become unreadable, and a broken image passed to url() can fail to display alt text while still contributing it to the accessible name—a violation of the Label in Name success criterion. Inline SVG is generally a better choice for icons, though browser support for hiding decorative generated content from the accessibility tree remains inconsistent.

/* like: <img src="icon.svg" alt=""> */
.icon {
  content: url('path/to/icon.svg') / "";
}

To keep generated content accessible:

  • Prefer HTML elements over CSS pseudo-elements for content that carries meaning.
  • When support is consistent, mark decorative or redundant generated content with an empty alt attribute or equivalent.

A Hidden Label Can Strip an Accessible Name

CSS can also take away an element's accessible name if the source of that name is hidden in a way that removes it from the accTree. An <input> that relies on a <label> for its name will wind up unnamed if the label is hidden with a method that hides from assistive technology.

Showing the HTML for a label-input pair and CSS that uses display: none to hide the label.

The AccName specification allows for a workaround. Assistive technologies do not relay hidden information by default, but authors can explicitly include hidden text in the accessible name or description through aria-labelledby or aria-describedby. A .visually-hidden approach keeps the label visible to assistive tech while hiding it visually.

Using aria-labelled by on an text form input with DevTools showing the input's accessible name which is pulled from the label element.

What CSS Cannot Do: States and Modal Semantics

Not everything can be accomplished with CSS alone. An element that toggles another piece of content needs its state exposed, and no stylesheet can do that. Content that appears on hover or focus must meet the requirements of the Content on Hover or Focus criterion: users must be able to dismiss it, and it must persist when the cursor moves toward it.

CSS-only techniques like the checkbox hack fail hard when used for modals. They do not trap focus, do not make background content inert, and cannot manage keyboard focus without JavaScript. The Popover API, for its part, only creates non-modal popovers. A true modal requires a <dialog>, and even then, something like a flyout navigation using popover needs explicit handling of focus and background content. Popover-based flyouts also risk covering focused elements and running afoul of the Focus Not Obscured criterion if they lay over other interactive content. The solution needs to go beyond what CSS alone provides: close the popover when focus leaves it, and be intentional about what happens when a user makes a selection—such as updating page colors via a <select>—so the change is communicated and the user can anticipate it.

The Practical Lesson

CSS hacks designed to substitute for JavaScript can introduce accessibility barriers that are far harder to detect than the visual ones they solve. JavaScript remains the appropriate tool for interactive behavior that needs focus management, inert content, or state changes. Choose the right tool for the job—HTML and CSS have their strengths, but they are not universal replacements for scripted interactivity.