Suggest an editImprove this articleRefine the answer for “How to hide elements visually but keep them accessible to screen readers”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Visually hidden but accessible** is a CSS technique that removes an element from visual layout while keeping it in the accessibility tree. ```css .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; } ``` **Key rule:** `display: none` and `visibility: hidden` hide from screen readers too. Use `.sr-only` for labels, hints, and skip links.Shown above the full answer for quick recall.Answer (EN)Image**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 ### Skip link with keyboard reveal ```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.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.