display: none vs visibility: hidden.
Space or no space.
Both hide an element. Both remove it from screen readers. The difference is what happens to the layout: display: none removes the element entirely — it takes no space, other content shifts up. visibility: hidden hides the element but preserves its space — the box is invisible, but the layout doesn't move. This page covers when to use each, why visibility: hidden is animatable, and the third option — opacity: 0 — that most developers don't know about.
Short answer: display: none removes the element from flow and from the accessibility tree. visibility: hidden hides the element but keeps its space and still removes it from the accessibility tree. Use display: none when the element should be gone. Use visibility: hidden when you want the layout preserved. Use opacity: 0 when you want the element invisible but still accessible (screen readers announce it). Use a visually-hidden class when you want it hidden visually but available to assistive tech.
✅ The four hiding methods
Each one hides differently.
display: none
No space, no render, no accessibility. The layout reflows as if the element doesn't exist.
visibility: hidden
Space preserved, invisible, not in accessibility tree. Layout doesn't shift.
opacity: 0
Invisible but still in flow and accessibility tree. Screen readers announce it. Clickable unless pointer-events: none.
Visually-hidden class
Moved off-screen (1px), still accessible. For "skip to content" links and labels without visible text.
✅ Before and after
The same list, two hiding methods.
.list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.item--hidden {
display: none;
}
HTML: ┌─────────────────┐ │ Item 1 │ ├─────────────────┤ │ Item 2 (hidden) │ ← display: none ├─────────────────┤ │ Item 3 │ └─────────────────┘ Visual result: ┌─────────────────┐ │ Item 1 │ ├─────────────────┤ ← no gap, no space │ Item 3 │ └─────────────────┘ ✅ Item 2 is gone. ✅ Item 3 has moved up to fill the space. ✅ The layout looks like a 2-item list. ✅ Screen readers see 2 items. ✅ No animation possible — the element is removed.
.list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.item--hidden {
visibility: hidden;
}
HTML: ┌─────────────────┐ │ Item 1 │ ├─────────────────┤ │ Item 2 (hidden) │ ← visibility: hidden ├─────────────────┤ │ Item 3 │ └─────────────────┘ Visual result: ┌─────────────────┐ │ Item 1 │ ├─────────────────┤ │ │ ← invisible, but space kept ├─────────────────┤ │ Item 3 │ └─────────────────┘ ✅ Item 2 is invisible. ✅ The space is preserved — Item 3 doesn't move. ✅ Screen readers still skip Item 2. ✅ Animatable — visibility participates in transitions. ✅ The layout looks like a 3-item list with a blank row.
✅ The visually-hidden pattern
For content that should be announced, not shown.
.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;
}
/* Use: */
<button>
<span class="sr-only">Search</span>
<svg aria-hidden="true">...</svg>
</button>
When to use sr-only: ✅ Icon-only buttons — label is visually hidden ✅ "Skip to content" links — appear on focus ✅ Form labels hidden by design but required ✅ Section headings that are visually redundant When NOT to use: ❌ display: none — removes from accessibility tree ❌ visibility: hidden — removes from accessibility tree ❌ opacity: 0 — screen readers announce, but the element still takes space (confusing for sighted users) sr-only is the ONLY correct way to hide visually while keeping the content available to screen readers. ⚠️ Also add :focus-visible styles to make skip links appear on keyboard focus.
Don't use display: none to hide labels or icons that need to be announced. Both display: none and visibility: hidden remove the element from the accessibility tree — screen readers skip them entirely. This is correct for "this element is not present right now" but wrong for "this element is visually hidden but still available to assistive tech." Icon-only buttons, "skip to content" links, and form labels without visible text all need the visually-hidden pattern, not display: none. If you're using display: none on a label, the input has no accessible name and screen reader users can't identify it.
⚠️ Common mistakes
The hiding mistakes that cost real time.
visibility: hidden when you want the space goneThe element still occupies layout. Fix: display: none.
display: none when you want to animateNot animatable. Fix: visibility: hidden or animate opacity + display together in JS.
display: none on a labelInput loses its accessible name. Fix: sr-only class.
visibility: hidden on a focusable elementStill focusable via keyboard — invisible but user sees focus ring on nothing. Fix: use display: none, or remove from tab order.
opacity: 0 and expecting it to hide from screen readersStill announced. Fix: display: none or visibility: hidden for hiding from all.
opacity: 0 without pointer-events: noneInvisible element still intercepts clicks. Fix: add pointer-events: none.
visibility: hidden is accessibleIt's not — screen readers skip it. Fix: sr-only.
Google treats hidden content as less important, or as spam if manipulative. Fix: hide only legitimate UI state.
❓ Frequently asked
What is the difference between display: none and visibility: hidden?
display: none removes the element entirely — it takes no space, doesn't render, and isn't announced by screen readers. visibility: hidden hides the element but keeps its space in the layout. Screen readers also skip visibility: hidden elements. Neither is focusable. Use display: none when you want the element and its space gone. Use visibility: hidden when you want the space preserved.
Does visibility: hidden hide from screen readers?
Yes. Both display: none and visibility: hidden remove the element from the accessibility tree. Screen readers skip both. If you want an element hidden visually but still announced to screen readers, use neither — use a visually-hidden class (position: absolute, width: 1px, height: 1px, overflow: hidden, clip) instead.
Which one can be animated?
visibility: hidden. It's a discrete property but participates in transitions — you can transition from visible to hidden with a delay, or use visibility to control when an animation starts and stops. display: none is not animatable — the element is removed from the render tree, so there's nothing to animate.
Why would I use visibility: hidden instead of opacity: 0?
Two differences. (1) opacity: 0 keeps the element in the accessibility tree — screen readers announce it. visibility: hidden removes it. (2) opacity can be animated smoothly; visibility is discrete. For pure visual hiding with no layout change, opacity: 0 is more flexible. For hiding from assistive tech, visibility: hidden is correct.
Does display: none affect SEO?
Indirectly. Google has warned that hidden content may be weighted less. In practice, Google is smart enough to understand legitimate uses (tab panels, accordions, mobile menus), and most sites use display: none for these without penalty. But if you stuff keywords in a hidden div, expect it to be treated as spam.
What is a visually-hidden class?
A CSS pattern that hides an element visually but keeps it available to screen readers. Common implementation: position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;. Use it for 'skip to content' links, form labels when the design doesn't show them, and headings that are visually redundant but structurally important.