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-onlyshrinks the element to 1px and pulls it out of visual flow;aria-hidden="true"does the opposite and blocks screen readers entirely.- Use
.sr-onlyfor hints, labels, and skip links. Usearia-hiddenfor icons and purely decorative elements. display: nonehides from everyone, including screen readers. That is the most common mistake.
Quick example
<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-onlyon a<span>referenced viaaria-describedby - Icon-only button where the icon needs a text label:
aria-hidden="true"on the icon,aria-labelon the button - Skip navigation link (keyboard shortcut to jump to main content):
.sr-onlyplus:focusto reveal on tab - Decorative background gradient or image:
aria-hidden="true"or remove from DOM entirely - State announcement in a dynamic toggle:
.sr-onlytext combined witharia-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
<!-- 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
<!-- 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
<!-- 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
// 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-hiddenfor icon labels in navbars - Tailwind CSS 3:
sr-onlyutility in form descriptions and radio groups - WordPress Gutenberg: skip links via
.screen-reader-text - React Aria (Adobe):
HiddenSelectuses off-screen spans for native select options - Material-UI v5: inline
sxstyles withposition: absolute; width: 1pxin 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
Skip link with keyboard reveal
<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
// 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 readyA concise answer to help you respond confidently on this topic during an interview.