Back to CSS
CSS
easy
junior

What is the difference between display none, visibility hidden, and opacity zero?

display:none removes from the layout AND a11y tree (no space, not focusable). visibility:hidden reserves space but hides + removes from a11y. opacity:0 reserves space and IS still interactive + announced — usually a bug if you wanted 'hidden'.

5 min read·~8 min to think through

Three CSS properties hide elements with completely different semantics. Picking the wrong one breaks layout, accessibility, or both.

display: none

  • Element is removed from the render tree.
  • Takes no space in layout.
  • Not focusable (Tab skips it), not announced by screen readers.
  • Children with display: block are also removed regardless.
  • Style/layout changes inside cost nothing.
  • Toggling triggers layout when shown again.

Use for: components that should be entirely gone (closed tabs, conditionally rendered sections, mobile-only nav at desktop sizes via media query).

visibility: hidden

  • Element is invisible but takes up its layout space.
  • Not focusable, not announced by screen readers.
  • Children can opt back in with visibility: visible.
  • Cheaper to toggle than display because layout is already known.

Use for: hiding an element while preserving layout (a tooltip slot, a placeholder that flips visible without layout shift).

opacity: 0

  • Element is fully transparent.
  • Takes layout space.
  • Still focusable, still announced by screen readers, still receives clicks (unless pointer-events: none).
  • Composited — animates cheaply on the GPU (just opacity, no layout/paint).

Use for: animations only. Rarely the right "hide" — it's a transparency, not a hide. If a button has opacity: 0, sighted users see nothing but tab + click still trigger it. That's a bug.

The accessibility table.

MethodLayout spaceFocusableSR announcesClick target
display:nonenononono
visibility:hiddenyesnonono
opacity:0yesyesyesyes
aria-hidden=trueyesyes (problem!)noyes
inert (attribute)yesnonono

aria-hidden="true" hides from the a11y tree but does NOT remove focusability — a focusable child with aria-hidden is a known bad combo (focusable but unannounced). Pair with tabindex="-1" or use the modern inert attribute (Safari 15.5+, Chrome 102+) which handles both.

Visually hidden but available to screen readers. Common pattern for icon-only buttons that need an accessible name:

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;
}

Tailwind ships this as .sr-only. Don't use display: none for this — screen readers won't announce it.

Performance differences.

  • display: none → re-show triggers layout from scratch.
  • visibility: hidden → layout already done; show is cheaper.
  • opacity: 0 → composited; animates without layout/paint.

For an animated reveal: opacity: 0 + transform is the right move. Combine with pointer-events: none while transparent so it's not clickable mid-fade.

Hidden attribute. <div hidden> is shorthand for display: none. Equivalent semantics, less specificity.

Toggle pattern (animated).

tsx
<div
  aria-hidden={!open}
  inert={!open as any}
  style={{ opacity: open ? 1 : 0, pointerEvents: open ? "auto" : "none", transition: "opacity 200ms" }}
>
  {children}
</div>

opacity for the transition; inert for a11y + focus blocking.

Code

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;
}

/* Usage */
<button>
  <Icon />
  <span class="sr-only">Close menu</span>
</button>
Visually hidden but accessible (sr-only)

Follow-up questions

  • Why is opacity:0 usually wrong as a 'hide'?
  • What's the inert attribute for?
  • How is `hidden` different from `display:none`?
  • Why do screen readers ignore visibility:hidden but not aria-hidden focusable elements?

Common mistakes

  • Hiding clickable elements with opacity:0 — still keyboard-accessible bug.
  • Using display:none for icon-only button labels — screen readers can't announce.
  • Setting aria-hidden on a focusable element without tabindex=-1.
  • Toggling display:none every keystroke — repeated layout cost.

Performance considerations

  • visibility:hidden cheaper to toggle than display:none.
  • opacity-only animations stay on the compositor (60fps).
  • Show/hide a giant subtree? Prefer display:none + lazy mount when possible.

Edge cases

  • display:none on a media element pauses playback in some browsers; visibility:hidden does not.
  • An <option> with display:none is hidden in <select> across modern browsers (recently improved).
  • Inert blocks pointer events too — don't use on something users still need to interact with.

Real-world examples

  • Modals: inert + aria-hidden on the backdrop's siblings to trap focus.
  • Tabs: switch tab content via display:none vs hidden vs visibility — many libraries use mount/unmount instead.

Senior engineer discussion

Senior signal: knowing which methods affect the a11y tree, the inert attribute, and the sr-only pattern.