How CSS Display Properties Affect What the Accessibility Tree Exposes

How display: none, visibility: hidden, and CSS-generated content each affect the accessibility tree, with the WCAG success criteria that govern them.

What Display and Visibility Do to the Accessibility Tree

CSS display properties determine whether a screen reader announces a form correctly or silently skips a required field. The browser engine builds two trees: the DOM for layout and scripting, and the accessibility tree that assistive technology consumes. CSS Display Module Level 3, section 7, defines how computed display values map to accessibility tree exposure. The CSS Speech Module governs how that text is spoken. Every value you write, display: none, display: contents, visibility: hidden, tells the engine whether an element exists for a screen reader. That decision is not a browser whim; it is specified behavior.

WCAG 1.3.1 info and relationships requires that information, structure, and relationships conveyed through presentation remain programmatically determinable. The common break: using display: none on a label or legend to hide it visually strips it from the accessibility tree. The input gets no accessible name. The fix is a visually-hidden class that clips the text but keeps it in the accessibility tree. WCAG 2.1.1 keyboard accessible demands focus order match visual order; flexbox order and grid order can divorce them. WCAG 2.4.3 focus order has the same requirement. Each of these success criteria has a CSS failure mode, and each one gets named here with the code that triggers it.

WCAG 1.3.1 Info and Relationships

WCAG 1.3.1 info and relationships catches most display-property mistakes. It requires that the meaning of a page survive when presentation is removed. Hide a label with display: none, and the input loses its programmatic label. The relationship between text and field is gone. The accessible name computation spec defines how the browser derives a control's name from aria-label, aria-labelledby, or a native label element. It finds nothing. The screen reader announces an unnamed edit field.

The fix keeps the label in the accessibility tree while hiding it visually. The class below is the standard sr-only pattern. It uses clip and absolute positioning rather than display: none.

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

The failure case is simpler: display: none on the label removes it from the accessibility tree entirely. Every screen reader user encounters an unlabeled input. That is a WCAG 1.3.1 failure. The sr-only class needs no fallback, it works in all engines. If you need the label truly gone, remove the input as well or give it an aria-label. display: none does not propagate a name.

Display None Removes from Accessibility Tree

display: none removes an element from the accessibility tree. This is documented behavior in CSS Display Module Level 3, section 6.2. The element and all its descendants are not rendered. They take no layout space. They are excluded from the accessibility tree. This is not a visual-only effect; the accessibility tree has no node for that element. Screen readers do not announce it. Users cannot navigate to it.

The mistake is using display: none to hide text that should remain available to assistive technology, such as a skip link or an extra description. The mistake compounds when you toggle display: none dynamically. Browsers can retain focusability in some cases when the value flips, so a focusable element that was display: none may still receive programmatic focus in an undefined way. WCAG 2.1.1 keyboard accessible fails if a user tabs to an element that is not perceivable.

The replacement for display: none is visibility: hidden when you need to hide visually but keep the element in the accessibility tree and in layout. That distinction is the entire subject of the next section. If you need the element gone from both trees, display: none is correct. You must also remove it from focus order by removing it from the DOM or using inert.

.hidden {
  display: none;
}

That one declaration is the failure. Use the sr-only class from the previous section instead.

Visibility Hidden Accessibility Impact

visibility: hidden is the inverse of display: none. It keeps the element in the accessibility tree and in layout, but hides it on screen. The element occupies space. Assistive technology that reads the accessibility tree will still encounter it. This is useful for screen-reader-only text that must not be seen. It is also a trap: the element is still in the tab order. A keyboard user can focus it without seeing it. That is a WCAG 2.4.3 focus order failure if the focus indicator is hidden.

The spec for visibility is in CSS Display Module Level 3. The accessibility tree mapping is the same section 7. The practical guidance: use visibility: hidden only when you need the element in the accessibility tree but not on screen. Never use it on interactive elements unless you also remove them from focus order with a tabindex or inert attribute.

The failure case is a button with visibility: hidden. It is in the accessibility tree, so a screen reader announces it. It is invisible on screen. A sighted keyboard user tabbing through the page sees the focus land on nothing. The fix: use the sr-only class for non-interactive text. For interactive elements, either remove them from the DOM or use hidden plus inert.

CSS Content Property Screen Reader Support

The `content` property is where most icon-font failures originate. It generates text via the CSS Speech Module, and the accessibility tree may include that generated text in the accessible name computation. For icon fonts, the common pattern is a span with a class that sets `content: "\f123"`. That is an unpronounceable private-use codepoint. Screen readers attempt to speak it, producing gibberish, or they read the codepoint name, which is worse.

WCAG 1.3.1 fails because the icon's meaning is conveyed only through the generated text. That text is not reliably exposed as an accessible name. The `content` property has inconsistent support: some engines include generated text in the accessibility tree, others do not. The spec delegates the decision to the accessibility API, so the outcome varies. The reliable fix is to pair the icon with visually-hidden text or an aria-label that provides the real name.

.icon {
  content: "\f123";
  font-family: "IconFont";
}

That generated character is what a screen reader tries to speak. It is not a name; it is noise. Replace it with Add to cart next to the icon, or set aria-label on the element.

Flexbox Order WCAG Failure

The flexbox `order` property and the grid `order` property change the visual order of elements without changing the DOM order. The accessibility tree follows the DOM order, not the visual order. A screen reader and a keyboard user navigate in the DOM sequence. A sighted user sees a different sequence. The divergence breaks the expectation that focus order matches the reading and visual order. This is a WCAG 2.4.3 focus order and WCAG 2.1.1 keyboard accessible violation.

The failure case is a flex container with three buttons where `order: -1` visually moves the third button to the first position. A keyboard user tabs to the DOM-first element, which appears third on screen. WCAG 2.4.3 requires focus order to be logical and predictable. This is not.

.container {
  display: flex;
}
.item-c {
  order: -1;
}

Fix it by reordering the DOM to match the visual order. Do not use `order` to solve a layout problem that should be a source-order problem. If you cannot change the DOM, aria-flowto exists, but the honest recommendation is to change the source order. The flexbox `order` property has no accessibility tree mapping that overrides DOM order. It is purely visual.

Display Contents Accessibility Tree Mapping

display: contents removes the element's own box from layout but retains its children in the accessibility tree. The spec in CSS Display Module Level 3, section 6.2, defines this: the element's box is not generated, but its children are treated as if they were children of the parent. This flattens wrapper elements for flexbox and grid layouts without changing the DOM.

The accessibility tree mapping for display: contents is where the bugs live. Firefox incorrectly removed elements with display: contents from the accessibility tree until a fix shipped in late 2018. The element's children were orphaned. That issue is resolved, but another subtlety remains: display: contents on an element with native semantics, such as a button or a heading, strips those semantics in some engines unless an explicit role attribute is present. The element is not in the accessibility tree, so its role is gone. Its children are not promoted to inherit that role.

This is a WCAG 1.3.1 failure if the wrapper carried meaning, such as a heading that groups a set of links. The fix is to add role="heading" and aria-level directly on the display: contents element. Or avoid display: contents on semantic elements entirely. The fallback is to remove the wrapper from the DOM and restructure the markup. That approach predates display: contents support.

.wrapper {
  display: contents;
}

That single line can strip the wrapper's semantics. Test with a screen reader before shipping.

How Display Values Affect Focus Order and Keyboard Navigation

Focus order is governed by the DOM order, not by the visual order that display and layout properties create. Use display: none on a focusable element, and it is removed from the accessibility tree. Browsers do not let you tab to it. That is expected behavior. The failure mode is toggling display: none via JavaScript and then trying to focus the element programmatically. Some engines allow it. That creates a focus target that is not perceivable, a WCAG 2.1.1 keyboard accessible violation.

visibility: hidden keeps the element in the accessibility tree. It remains focusable. That is a real trap: a keyboard user can tab to an invisible button. Avoid visibility: hidden on interactive elements, or add tabindex="-1" and manage focus manually. The rule: if it is focusable, it must be visible. If it is hidden with display: none, remove it from focus order entirely. Do not toggle it back on before the user expects it.

The semantic HTML baseline is your ally. A native button or input has keyboard behavior built in. When you hide it with display: none, you lose that behavior. The accessible name computation will not find a name for a hidden input. The browser is the final renderer, and its behavior for dynamically toggled display: none may differ from the spec's intent.

Hiding Content Without Breaking Accessibility

Choosing the Right Hiding Method

Hiding content is a spectrum. display: none removes it from the accessibility tree. visibility: hidden keeps it in the accessibility tree but hides it on screen. The sr-only class keeps it in the accessibility tree, in layout, but visually clipped. The sr-only class is the one that satisfies WCAG 1.3.1 for labels and descriptions that must be available to assistive technology but not visible.

The sr-only class uses position: absolute and a 1px clip box. It takes no visual space. It remains in the accessibility tree. The text is announced by screen readers. It is not in the tab order unless it contains a focusable element, so it does not create a focus order problem. This is the pattern for adding a label to an icon-only button, adding a description to a complex widget, or providing a skip link that is invisible until focused.

The Skip Link Pattern

Use the sr-only class for text that is a supplement, not for interactive elements. If the hidden text is a link or a button, you must reveal it on focus. That requires a :focus rule that moves it into view. That is the standard skip-link pattern. The failure case is using display: none for a skip link. It removes the link from the accessibility tree and makes the page unskippable for keyboard users.

<a href="#main" class="sr-only">Skip to main content</a>

That link is invisible until focused. The focus rule brings it into view. It is in the accessibility tree, so a screen reader announces it. It is the correct fallback for a skip link.

When Display None Is the Right Choice

display: none is not always wrong. It is the correct choice when the content is truly irrelevant to assistive technology: a decorative element, a toggled-off panel that is not needed for understanding, or a duplicate of text that exists elsewhere. The WCAG technique for hiding content that is not part of the current view, such as a tab panel that is not active, is to use display: none. The content is not relevant until the tab is selected.

The mistake is hiding content that is relevant but visually redundant. A label for a search input that has a visible placeholder is the classic case. The placeholder is not a label. It is not programmatically associated with the input, and it does not satisfy WCAG 1.3.1. The visible placeholder does not replace the label. The fallback for a visually hidden label is the sr-only class, not display: none.

The rule: if the content is needed for understanding or operation, keep it in the accessibility tree. If it is not, display: none is safe. When you are unsure, choose the sr-only class. It does not remove content from the accessibility tree. It is the conservative choice.

Frequently Asked Questions

Does display: none remove an element from the tab order?

Yes. The element is not in the accessibility tree, and browsers do not allow tabbing to it. The exception is scripted focus on a dynamically toggled element, which can behave inconsistently. Test with a real keyboard.

Is visibility: hidden the same as display: none for screen readers?

No. visibility: hidden keeps the element in the accessibility tree. It is hidden on screen. Screen readers can still announce it. That is useful for non-interactive text but dangerous for focusable elements.

Does the content property always produce accessible text?

No. Generated text is not reliably included in the accessible name computation. Icon fonts produce unpronounceable characters. Use visually hidden text or an aria-label for real names.

How do I fix a flexbox order focus order problem?

Change the DOM order to match the visual order. The order property is visual only and does not affect the accessibility tree. There is no CSS-only fix that reorders focus without breaking WCAG 2.4.3.