Skip to main content

Difference between visibility: hidden and display: none

display: none removes an element from the layout flow entirely. visibility: hidden hides it but preserves its space in the document.

Theory

TL;DR

  • display: none is like erasing a chair from a room: other furniture shifts to fill the gap
  • visibility: hidden is like draping an invisible cloth over the chair: the space stays empty
  • Main difference: does the element hold its space after being hidden
  • Need stable layout while hiding? Use visibility: hidden. Removing completely? Use display: none
  • visibility can be animated with opacity; display cannot be transitioned at all

Quick example

html
<!-- display: none - Box 3 moves up, gap is gone --> <div class="container"> <div class="box">Box 1</div> <div class="box" style="display: none;">Box 2</div> <div class="box">Box 3</div> </div> <!-- visibility: hidden - Box 3 stays in place --> <div class="container"> <div class="box">Box 1</div> <div class="box" style="visibility: hidden;">Box 2</div> <div class="box">Box 3</div> </div>

In the first container, Box 3 slides up to where Box 2 was. In the second, Box 3 stays exactly in place.

Key difference

display: none tells the browser to skip the element entirely during layout. No box model is generated: no width, no height, no position. Surrounding elements reflow as if it never existed. visibility: hidden still goes through the full layout pass, computes its dimensions, holds its space in the flow, but paints nothing. Toggling display triggers a full layout reflow. Toggling visibility only triggers a repaint, which is noticeably cheaper.

When to use

  • No layout shift needed (tooltips, hover previews, aligned dropdowns): visibility: hidden
  • Full removal from flow (modals, conditional sections, dynamic list items): display: none
  • Smooth fade animation: combine visibility: hidden with opacity: 0
  • Screen reader and tab order removal: display: none paired with aria-hidden="true"

Comparison table

Aspectdisplay: nonevisibility: hidden
Space in layoutNo (others reflow)Yes (placeholder stays)
Box model generatedNoYes
AnimatableNoYes (with opacity)
Screen readerSkippedSkipped
Inherits to childrenNoYes (overridable)
Triggers reflowYesNo (repaint only)
When to useModals, collapse menus, conditional renderTooltips, hover previews, dropdown alignment

How browsers handle this

Chrome's Blink engine skips display: none elements entirely in the layout phase. They never enter the containing block formatting context, so siblings calculate positions as if those elements do not exist. visibility: hidden elements go through layout normally and get computed dimensions. The paint phase simply skips drawing them. That is the source of the reflow difference: one property affects layout, the other only affects paint.

Common mistakes

Trying to animate display

css
.menu { transition: opacity 0.3s; /* this works */ display: none; /* this does not animate */ }

display is a discrete property. The CSS Transitions spec does not allow transitioning it, so any transition on an element with display: none is ignored. The fix is opacity + visibility:

css
.menu { opacity: 1; visibility: visible; transition: opacity 0.3s, visibility 0.3s; } .menu.hidden { opacity: 0; visibility: hidden; }

Forgetting visibility inheritance

css
.parent { visibility: hidden; } /* all children hidden too */ .child { visibility: visible; } /* this DOES work - child becomes visible */

Unlike display: none, visibility can be overridden on children. Setting visibility: visible on a specific child makes it show up even when the parent is hidden. This is useful when you need to reveal one element inside a hidden group.

Leaving focusable elements inside visibility: hidden

Both properties hide content from screen readers. But visibility: hidden does not remove elements from tab order. A hidden button inside a visibility: hidden container is still reachable by keyboard. display: none removes it from tab order completely. For form fields or buttons you want fully hidden, display: none is the safer choice.

Flexbox space not collapsing

css
.flex-container { display: flex; } .flex-child { flex: 1; } .flex-child.hidden { visibility: hidden; } /* still takes its flex share */

With visibility: hidden, the hidden child keeps its flex allocation and siblings do not expand. With display: none, siblings grow to fill the freed space. This trips developers working on equal-column layouts more often than expected.

Real-world usage

  • React: display: none via conditional rendering ({isOpen && <Modal />}) for complete removal
  • Tailwind: hidden class = display: none; invisible class = visibility: hidden
  • Bootstrap: accordion collapse uses display: none to fully remove panel content
  • Vue: v-show sets display: none; v-if removes the element from the DOM entirely
  • Tooltip libraries: visibility: hidden + opacity: 0 to avoid layout jumps on first show

Follow-up questions

Q: What triggers a reflow: toggling display or visibility?
A: Toggling display triggers a full layout reflow. Toggling visibility only triggers a repaint. That is why visibility is cheaper for frequent show/hide cycles.

Q: Can you animate from display: none to display: block?
A: No. display is discrete and cannot be transitioned. Use opacity and visibility together instead.

Q: Can a child override visibility: hidden set on a parent?
A: Yes. Setting visibility: visible on a child makes it visible even if the parent is hidden. This does not work with display: none.

Q: In a flex container, what happens to siblings when one item gets visibility: hidden?
A: They keep their positions. The hidden item still holds its flex share, so siblings do not expand. With display: none, siblings would grow to fill the freed space.

Q: Which is better for a tooltip that toggles frequently?
A: visibility: hidden with opacity. It only triggers a repaint on toggle, not a full layout recalculation. The visibility + opacity combo is what I use in production tooltips by default - the transition stays smooth even at high frequency.

Examples

Layout difference in a flex row

html
<style> .row { display: flex; gap: 10px; margin-bottom: 20px; } .box { width: 100px; height: 100px; background: lightblue; display: flex; align-items: center; justify-content: center; } </style> <!-- display: none - the gap is gone --> <div class="row"> <div class="box">1</div> <div class="box" style="display: none;">2</div> <div class="box">3</div> </div> <!-- Renders: [1][3] --> <!-- visibility: hidden - the gap stays --> <div class="row"> <div class="box">1</div> <div class="box" style="visibility: hidden;">2</div> <div class="box">3</div> </div> <!-- Renders: [1][ ][3] -->

Box 3 sits in a different position in each row. That single visual difference determines which property to reach for.

Smooth tooltip with visibility + opacity

jsx
function Tooltip({ children, text }) { const [visible, setVisible] = useState(false); return ( <div style={{ position: 'relative', display: 'inline-block' }}> <button onMouseEnter={() => setVisible(true)} onMouseLeave={() => setVisible(false)} > {children} </button> <div style={{ position: 'absolute', top: '110%', left: 0, padding: '4px 8px', background: '#222', color: '#fff', borderRadius: 4, whiteSpace: 'nowrap', visibility: visible ? 'visible' : 'hidden', opacity: visible ? 1 : 0, transition: 'opacity 0.2s, visibility 0.2s', }}> {text} </div> </div> ); } // visibility: hidden keeps dimensions computed - tooltip position stays stable on every show // display: none would recalculate position on each show, causing misalignment on first hover

The tooltip stays in the right position every time because its dimensions are always computed, just not painted. Switching to display: none here causes a visible flicker on first hover because the browser has to calculate the tooltip size from scratch.

Short Answer

Interview ready
Premium

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

Finished reading?