Skip to main content

How to hide elements visually but keep them accessible to screen readers

Visually hidden means an element is removed from the visual layout but stays in the DOM and the accessibility tree, so screen readers like NVDA or VoiceOver still announce it.

Theory

TL;DR

  • Think of it as a whisper in a noisy room: sighted users see nothing, screen readers hear every word.
  • .sr-only shrinks the element to 1px and pulls it out of visual flow; aria-hidden="true" does the opposite and blocks screen readers entirely.
  • Use .sr-only for hints, labels, and skip links. Use aria-hidden for icons and purely decorative elements.
  • display: none hides from everyone, including screen readers. That is the most common mistake.

Quick example

html
<style> .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; } </style> <button>Submit</button> <!-- Screen reader announces this after the button label --> <span class="sr-only">Press Enter to submit the form</span>

Sighted users see only "Submit". NVDA and VoiceOver read "Press Enter to submit the form" right after the button.

Key difference: .sr-only vs aria-hidden

.sr-only uses position: absolute to pull the element out of visual flow, then clip: rect(0,0,0,0) to confine it to a 1px painted area. The DOM and the accessibility tree stay untouched, so screen readers traverse it normally. aria-hidden="true" is the reverse: it marks the node as invisible to assistive tech while leaving it on screen. Bootstrap ships it as .visually-hidden, Tailwind as sr-only. Same pattern, different names.

When to use

  • Form hint or validation rule for screen reader users only: .sr-only on a <span> referenced via aria-describedby
  • Icon-only button where the icon needs a text label: aria-hidden="true" on the icon, aria-label on the button
  • Skip navigation link (keyboard shortcut to jump to main content): .sr-only plus :focus to reveal on tab
  • Decorative background gradient or image: aria-hidden="true" or remove from DOM entirely
  • State announcement in a dynamic toggle: .sr-only text combined with aria-expanded

How browsers handle this

position: absolute removes the element from normal flow. clip: rect(0,0,0,0) is deprecated but still supported across Chrome, Firefox, and Safari. The modern replacement is clip-path: inset(100%), which handles transformed elements better. Screen readers access the accessibility tree through DOM APIs like computedRole and computedLabel. CSS layout does not affect that tree. aria-hidden does, because it sends a direct signal to the accessibility API.

No JavaScript needed. The hiding happens at parse time, purely through CSS.

Common mistakes

Using display: none and expecting screen readers to still read it

html
<!-- Wrong: removed from the accessibility tree entirely (W3C UAAG) --> <span style="display: none;">Hint text</span>

Fix: swap to .sr-only.

Assuming visibility: hidden is accessible

html
<!-- Wrong: NVDA and VoiceOver skip this, same as display: none --> <span style="visibility: hidden;">Hint text</span>

Fix: .sr-only.

aria-hidden="true" on an interactive element

html
<!-- Wrong: keyboard users cannot focus or activate this button --> <button aria-hidden="true">Submit</button>

Chrome 89+ enforces focus blocking on elements with aria-hidden. Interactive elements must never carry it.

aria-hidden on a wrapper that contains focusable children

jsx
// Wrong: button is unreachable for assistive tech but still in the tab order <div aria-hidden="true"> <button>Close</button> </div>

Use the inert attribute (Chrome 102+) instead. It blocks both focus and the accessibility tree at once, without the subtree trap.

The display: none mistake shows up in accessibility audits more than you would expect. It is the first thing axe DevTools flags, and it usually means someone assumed hidden means hidden-but-still-readable.

Real-world usage

  • Bootstrap 5: .visually-hidden for icon labels in navbars
  • Tailwind CSS 3: sr-only utility in form descriptions and radio groups
  • WordPress Gutenberg: skip links via .screen-reader-text
  • React Aria (Adobe): HiddenSelect uses off-screen spans for native select options
  • Material-UI v5: inline sx styles with position: absolute; width: 1px in Drawer components

Follow-up questions

Q: What is the difference between clip: rect(0,0,0,0) and clip-path: inset(100%)?
A: clip is deprecated. Firefox 112 removed it. clip-path: inset(100%) handles transformed elements better. Keep both in the same class for broad support during the transition.

Q: Does .sr-only work inside flex or grid containers?
A: Yes. position: absolute removes the element from flex or grid flow. Test reading order with NVDA on Windows Chrome to confirm.

Q: When should I use aria-label instead of a .sr-only span?
A: Use aria-label for short text on a single interactive element. Use a .sr-only span for multi-line descriptions or when the text needs to be associated via aria-describedby.

Q: How do you handle aria-hidden on a modal backdrop?
A: The backdrop gets aria-hidden="true". The modal panel itself does not. If aria-hidden ends up on a wrapper containing the close button, switch to inert. It blocks both focus and the accessibility tree without the subtree problem.

Q: Is there a difference between VoiceOver on macOS and NVDA on Windows when reading .sr-only content?
A: Both pull text from the DOM via their own APIs. VoiceOver prioritizes aria-describedby relationships. NVDA falls back to innerText when no ARIA relations exist. Test on both platforms when the announcement order matters.

Examples

html
<style> .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; } /* Reveal when focused via keyboard Tab */ .sr-only:focus { position: static; width: auto; height: auto; background: white; color: black; padding: 0.5rem; clip: auto; } </style> <a href="#main" class="sr-only">Skip to main content</a> <main id="main">Page content</main>

Tab reveals the link at the top of the page for keyboard users. Touch devices may not trigger :focus styles, so test across platforms. This pattern comes from WebAIM's ARIA techniques and is used in Gutenberg.

React login form with hidden password hint

jsx
// Login form: visible label, password rules hidden from sighted users function LoginForm() { return ( <form> <label htmlFor="password">Password</label> <input id="password" type="password" aria-describedby="password-rules" /> {/* Invisible to sighted users; NVDA/VoiceOver reads after input focus */} <span id="password-rules" className="sr-only"> Use 8 or more characters with a number and symbol </span> <button type="submit">Login</button> </form> ); }

NVDA announces: "Password, edit, Use 8 or more characters with a number and symbol". The form stays visually clean for sighted users. This pattern mirrors GitHub's and Bootstrap React's login form approach.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?