Visually Hiding Content for Screen Readers Without Removing It from the Accessibility Tree

The CSS clip-rect pattern for hiding content visually while keeping it in the accessibility tree, and why display: none is the wrong tool.

The Problem and the Fix

You have a skip link that should let keyboard users jump past your navigation. The moment you hide it with display: none you have thrown it out of the accessibility tree and the screen reader never announces it. The one question this page answers is how to hide that content visually while keeping it readable by assistive technology. The answer is a four-line CSS declaration block that has been stable for decades. The approach goes by the name CSS visually hidden screen reader content. It is the difference between a keyboard user who can skip your menu and one who must tab through every item. The correct technique is the .sr-only class defined in the WAI-ARIA Authoring Practices guide, not a blog post's opinion. It uses clip or clip-path to remove the element from the visual rendering without removing it from the accessibility tree. If you have ever written display: none on a skip link and watched your screen reader test fail, this is the section that names the mistake and hands you the fix.

What the Accessibility Tree Actually Is

The accessibility tree is the browser's translation of your DOM into what assistive technology can consume. It is not the same as your visual layout. A screen reader announces what the accessibility tree tells it, not what your eyes see. A CSS declaration that hides an element visually but leaves it in the tree is the only way to get the words read without the pixels shown. The classic offender is display: none. It removes the element from both the visual rendering and the accessibility tree. The same is true of visibility: hidden and opacity: 0 applied to an ancestor. The clip-rect approach works because clip creates a rectangular clipping region that hides the element from view. The element itself remains in the accessibility tree for the screen reader to announce. This is why the WCAG 2.1.1 success criterion about keyboard access and the WCAG 1.3.1 criterion about info and relationships both rely on this technique: the content is present in the tree, so its relationships and meaning are preserved even though no pixel shows it.

WCAG 2.4.1: The Bypass Block Requirement

The WCAG 2.4.1 success criterion, titled Bypass Blocks, is the specification that makes your skip link necessary at all. The criterion requires a mechanism to bypass blocks of content that are repeated on multiple pages. The most common mechanism is a skip navigation link: a link that appears visually only when it receives keyboard focus. The CSS visually hidden screen reader content technique is the enabler. The skip link is visually hidden in its resting state, then revealed on :focus. That is what the WCAG 2.4.1 criterion demands. The failure you are trying to avoid is a skip link invisible to the screen reader because display: none removed it from the accessibility tree. A keyboard user cannot use it at all. The correct approach, with the .sr-only class clipped via clip-path or clip, keeps the link in the tree so the screen reader announces it. The :focus rule restores its visibility so a sighted keyboard user sees the link appear when they tab to it. The clipped approach beats a text-indent hack because it does not trigger a layout recalculation when the element receives focus. That recalculation is the exact failure of the older position: absolute; left: -9999px approach.

WCAG 1.3.1: Info and Relationships

The WCAG 1.3.1 success criterion, named Info and Relationships, is what you honour when you keep hidden content in the accessibility tree. The criterion requires that information, structure, and relationships conveyed through presentation are also available in text. That includes content that is visually hidden but still present in the DOM. When you hide a label with the .sr-only approach, you are not removing it. You are letting the screen reader announce a relationship that a sighted user sees as layout. That is the essence of the WCAG 1.3.1 requirement. The failure of display: none is that it breaks this criterion: the content is no longer in the accessibility tree. The relationship between a form field and its visually hidden label is lost to the assistive technology. The clip-rect approach preserves that relationship. It is the recommended technique for hiding content like form labels, icon-button text, and table headers that need to be read but not seen.

The Canonical .sr-only Clip-Rect Pattern

How the Declaration Block Works

Here is the complete .sr-only class, attributed to the WAI-ARIA Authoring Practices guide.

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

position: absolute takes the element out of normal document flow. That stops the hidden content from pushing the layout. It also establishes a positioning context that the clip and clip-path properties can act on. The width and height of 1px, combined with padding: 0 and margin: -1px, create a 1-by-1-pixel box that is effectively invisible. The negative margin pulls it into a corner so that no residual box appears. overflow: hidden ensures that any content that would spill out of that 1px box is clipped rather than painted onto the page.

Legacy and Modern Clip Properties

clip: rect(0, 0, 0, 0) is the legacy declaration, supported since CSS 2.0 in 1998. It sets a zero-sized rectangular region, which hides the element visually. clip-path: inset(50%) is the modern replacement, supported in all engines for years, and it does the same job: it insets the element's box by 50% on each side, shrinking it to a zero-area rectangle. Check caniuse.com for the current support baseline.

The Property That Is Easy to Forget

white-space: nowrap is the one declaration that trips people up. Without it, long text wraps into a visible 1px-tall column that a sighted user can see. border: 0 removes any default border that could produce a visible outline on a 1px box. The result is a class that hides content visually while keeping it in the accessibility tree. Use it for every visually hidden screen reader content need.

The Broken display: none on a Skip Link

Now the failure case: the skip link that uses display: none. It removes the link from the accessibility tree, so the screen reader never announces it.

<a href="#main" class="skip-link">Skip to main content</a>

<style>
.skip-link {
  display: none;
}
</style>

The problem is not that the link is invisible. Invisibility is fine. The problem is that display: none removes the element from the accessibility tree. A screen reader user tabbing through the page never hears the link announced. The WCAG 2.4.1 criterion is violated because there is no bypass mechanism available to that user. The same failure occurs with visibility: hidden, which also removes the content from the accessibility tree. It also occurs with opacity: 0 on an ancestor, because opacity creates a stacking context that can hide the element from assistive technology. The fix is to replace display: none with the .sr-only class. That keeps the link in the accessibility tree. Then add a :focus rule to reveal it visually when the keyboard user tabs to it. That is the technique shown in the next sample.

The .sr-only--focusable Extension for Skip Links

For a skip navigation link, you need the .sr-only class plus a modifier that reveals the link on :focus. The point of the link is to appear when a keyboard user tabs to it.

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

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

.sr-only--focusable:active,
.sr-only--focusable:focus {
  position: static;
  width: auto;
  height: auto;
  margin: 0;
  overflow: visible;
  clip: auto;
  clip-path: none;
  white-space: normal;
}
</style>

The .sr-only--focusable class resets every property that was hiding the element. position becomes static. width and height become auto. margin becomes 0. overflow becomes visible. Both clip and clip-path are turned off. The result: the link appears in normal flow, visible and clickable, exactly when a keyboard user tabs to it. padding, border, and white-space are also reset to their default values so the link renders as a normal anchor. This approach passes WCAG 2.4.1. The bypass mechanism is present in the accessibility tree and becomes visually available on focus. The focusable modifier is what makes the difference between a hidden label that no one needs to see and a skip link that a keyboard user must see.

Why the clip-rect Pattern Replaces the text-indent Hack

The older technique for visually hiding content was text-indent: -9999px. It pushes the text off the left edge of the viewport. That hack fails because the focus ring, drawn by the browser around the element's box, extends off-screen. It becomes invisible or positioned in a way that disorients the user. When a skip link uses text-indent: -9999px, the focus ring is drawn at the off-screen location. A keyboard user who tabs to the link sees no visible indicator of where the focus is. That fails the WCAG 2.4.7 focus visible criterion. The clip-rect approach keeps the focus ring attached to the element's on-screen position when it is revealed on :focus. The element returns to normal flow with the focusable modifier. The other advantage is performance. text-indent causes the browser to perform a layout recalculation when the element receives focus, because the content's position changes. The clip approach only changes the clipping region. That is a paint-only operation with no layout cost. clip and clip-path do not affect layout at all. They only determine what gets painted from the element's box. The approach is both more robust and cheaper than the text-indent hack.

What the Research Attributes to Whom

Authoritative Sources

The clip-rect technique is not the invention of a blog post. It is documented in the WAI-ARIA Authoring Practices guide, the W3C's official source for building accessible widgets and navigation. The declaration block itself is also captured in MDN Web Docs, on the Inclusively Hidden page, last reviewed in 2025. The WCAG 2.2 Techniques document lists a variant as technique C7, Using CSS to hide a portion of the link text.

Browser Support and the Fallback

The clip-path: inset(50%) alternative shipped first in WebKit, then Blink, and then Gecko. It is now widely available; check caniuse.com for the current baseline. The clip: rect() property is deprecated, but you keep it in the declaration block before clip-path. Older browsers, specifically Internet Explorer, never supported clip-path and still get a working hide from clip. The combination degrades safely: any engine that does not support clip-path still honours clip. Any engine that supports clip-path uses it. The approach works everywhere.

The Wrap Trap

The mistake of omitting white-space: nowrap is well-documented. Without it, a long phrase wraps into a visible 1px-tall column that defeats the purpose of hiding. The authoritative sources are the W3C's Techniques for WCAG 2.2 and the MDN Inclusively Hidden page. The support data referenced here comes from caniuse.com, accessed 2026.

Common Mistakes and How to Avoid Them

The first mistake is using display: none or visibility: hidden on a skip link. It removes the content from the accessibility tree and fails WCAG 2.4.1. Use the .sr-only class instead. The second mistake is forgetting the white-space: nowrap declaration. The text wraps into a visible column when the content is longer than one line. Include it in every .sr-only block. The third mistake is using the focusable modifier without resetting every property. The link remains clipped even on focus. Copy the full reset from the sample above. A fourth mistake, common in modern codebases, is using clip-path: inset(50%) alone without the clip: rect() fallback. It works in all modern browsers but fails in legacy Internet Explorer. The fallback is necessary for older enterprise environments. A fifth mistake is applying the .sr-only class to an interactive element like a button or a link that does not have the focusable modifier. That makes the control permanently invisible to keyboard users. Use the focusable variant for anything that needs to receive focus.

Performance and Practical Costs

The performance cost of the .sr-only approach is negligible. It is a single element with a 1px box. The clip or clip-path properties are paint-only. They do not trigger layout or composite. The cost is effectively zero. Layout cost is none: position: absolute takes the element out of flow and the 1px box does not reserve space. Paint cost is low: the element is clipped to zero area, so nothing is drawn. In contrast, the text-indent: -9999px hack triggers a layout cost at the moment of focus. The browser must recalculate the position of the off-screen content. It can cause a visible scrollbar jump in some browsers. The clip-rect approach has no such cost. Focus does not change the element's position; it only changes the clipping region. For a skip link, a single element on the page hidden most of the time, the performance impact is unmeasurable. The real cost is the risk of getting the approach wrong and failing an accessibility audit.

Who This Topic Suits and Who It Does Not

This material suits the working front-end developer who writes CSS daily and needs a reliable, spec-aligned answer without reading five blog posts. It suits the performance-conscious developer who wants to know that clip and clip-path carry no layout cost. It suits the technical writer who needs to cite the WAI-ARIA Authoring Practices guide and the WCAG 2.2 techniques: the approach is sourced, not guessed. It suits the educator who wants to explain the difference between visual hiding and accessibility-tree removal. The concept is precise and testable. It does not suit someone learning to code from zero. That person should first learn the basics of CSS layout and the accessibility tree. It does not suit someone debugging a JavaScript state bug. The failure is not in the script but in the stylesheet. It does not suit anyone comparing CSS-in-JS libraries. The runtime behaviour of the browser is the same regardless of whether the class is in a stylesheet or a template literal. If you are a developer who has ever written display: none on a label, or used text-indent: -9999px on a link, then this is the page that tells you what to do instead. If you are not, you can skip it.