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: noneis like erasing a chair from a room: other furniture shifts to fill the gapvisibility: hiddenis 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? Usedisplay: none visibilitycan be animated withopacity;displaycannot be transitioned at all
Quick example
<!-- 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: hiddenwithopacity: 0 - Screen reader and tab order removal:
display: nonepaired witharia-hidden="true"
Comparison table
| Aspect | display: none | visibility: hidden |
|---|---|---|
| Space in layout | No (others reflow) | Yes (placeholder stays) |
| Box model generated | No | Yes |
| Animatable | No | Yes (with opacity) |
| Screen reader | Skipped | Skipped |
| Inherits to children | No | Yes (overridable) |
| Triggers reflow | Yes | No (repaint only) |
| When to use | Modals, collapse menus, conditional render | Tooltips, 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
.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:
.menu {
opacity: 1;
visibility: visible;
transition: opacity 0.3s, visibility 0.3s;
}
.menu.hidden {
opacity: 0;
visibility: hidden;
}Forgetting visibility inheritance
.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
.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: nonevia conditional rendering ({isOpen && <Modal />}) for complete removal - Tailwind:
hiddenclass =display: none;invisibleclass =visibility: hidden - Bootstrap: accordion collapse uses
display: noneto fully remove panel content - Vue:
v-showsetsdisplay: none;v-ifremoves the element from the DOM entirely - Tooltip libraries:
visibility: hidden+opacity: 0to 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
<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
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 hoverThe 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 readyA concise answer to help you respond confidently on this topic during an interview.