What display: none Actually Does

display: none removes an element from the layout entirely. That means:

  • It takes up zero space (as if it’s not in the HTML).
  • It is not visible.
  • It is not clickable and can’t be tabbed to.
  • Its children are also removed from layout.

This makes it perfect for things that should be “not there” (like a closed dropdown panel), but it’s also why it’s not great for smooth transitions: there’s nothing to animate when something doesn’t exist in layout.

.demo .badge {
  display: none;
}
.demo .badge {
  display: inline-block;
}
.demo .badge {
  display: block;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding: 16px;
  border: 2px solid #111;
  border-radius: 12px;
  max-width: 520px;
}

.badge {
  padding: 6px 10px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #f2f2f2;
  font-weight: 700;
}

.note {
  margin-top: 12px;
  padding: 10px 12px;
  border: 1px dashed #111;
  border-radius: 10px;
  background: #fff;
}
I am a badge
Switch snippets to see how display changes layout.

CSS display: none VS hidden (HTML Attribute)

The HTML hidden attribute is basically a built-in “hide this element” switch. In browsers, it behaves like:

  • [hidden] { display: none; }

So hidden and display: none often produce the same visual result. The difference is mainly how you control it:

  • hidden is an HTML attribute. Great for toggling state in markup or JS.
  • display: none is CSS. Great for styling rules, responsive rules, and conditional display via selectors.
.card[hidden] {
  display: none;
}
.card[hidden] {
  display: block;
}
.card[hidden] {
  display: block;
  opacity: 0.35;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  display: grid;
  gap: 12px;
  max-width: 560px;
}

.card {
  border: 2px solid #111;
  border-radius: 12px;
  padding: 14px;
  background: #f2f2f2;
}

.card strong {
  display: inline-block;
  margin-bottom: 6px;
}

.hint {
  border: 1px dashed #111;
  border-radius: 12px;
  padding: 12px 14px;
}
In browsers, hidden behaves like display: none. In these snippets we override it to prove it’s just CSS under the hood.

CSS display: none vs visibility: hidden

These two hide things in very different ways:

  • display: none: removes the element from layout (no space).
  • visibility: hidden: element is invisible, but it still keeps its space.

This is the reason your layout “jumps” when you use display: none, but it doesn’t when you use visibility: hidden.

.item.is-hidden {
  display: none;
}
.item.is-hidden {
  visibility: hidden;
}
.item.is-hidden {
  opacity: 0;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.row {
  font-family: ui-sans-serif, system-ui, sans-serif;
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  padding: 14px;
  border: 2px solid #111;
  border-radius: 12px;
  max-width: 560px;
}

.item {
  width: 140px;
  padding: 12px 10px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #f2f2f2;
  text-align: center;
  font-weight: 700;
}
A
C
D

Extra: Clickability and Focus

Both display: none and visibility: hidden make the element non-interactive. But opacity: 0 alone does not. That means you can accidentally create “invisible buttons” unless you also disable pointer events.

Try hovering the button to the right of the Normal button, with snippet 1 active.

.ghost {
  opacity: 0;
}
.ghost {
  opacity: 0;
  pointer-events: none;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.box {
  font-family: ui-sans-serif, system-ui, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 14px;
  max-width: 520px;
}

button {
  padding: 10px 12px;
  border: 2px solid #111;
  border-radius: 10px;
  background: #f2f2f2;
  font-weight: 700;
  cursor: pointer;
}

Learn more about visibility in the CSS Visibility Interactive Tutorial.

CSS display: none Transition

Here’s the big rule:

You cannot transition display.

Why? Because transitions animate between numeric/interpolatable values. display is not numeric and the browser can’t animate “not in layout” into “in layout”. It’s either there or it isn’t.

The common solution: animate opacity and size instead

A practical pattern is:

  • Use opacity for the fade.
  • Use something like max-height (or transform) for the “expand/collapse” feel.
  • Use pointer-events to prevent interaction while hidden.
.wrap .panel {
  max-height: 0px;
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  overflow: hidden;
  transition: max-height 350ms ease, opacity 250ms ease, transform 350ms ease;
}

.wrap:hover .panel {
  max-height: 200px;
  opacity: 1;
  transform: translateY(0px);
  pointer-events: auto;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 14px;
  max-width: 560px;
}

.panel {
  margin-top: 12px;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 12px;
  background: #f2f2f2;
}

.panel p {
  margin: 0;
}

.small {
  margin-top: 8px;
  font-size: 14px;
  opacity: 0.85;
}

.hint {
  margin-top: 10px;
  font-size: 14px;
  opacity: 0.85;
}
Panel title

This panel animates on hover without using display.

We fake “collapse” with max-height, and fade with opacity.

Hover this box to open the panel.

A small note about max-height

If you want a “perfect” height animation without guessing a max-height, you usually reach for JavaScript (measure content height and animate to that number), or you use different layout tricks (See this Chrome Dev article on animating width and height to auto, and this Keith J Grant article). But for many, max-height is a solid starting point.

CSS display: none Animation

Just like transitions, display cannot be animated. But you can still create an “enter/exit animation” by animating other properties and only using display: none at the very end.

CSS-only: animate a popover with hover (no display)

This is the simplest approach: keep the element in layout (or position it absolutely), then animate opacity + transform.

.popover {
  transition: opacity 200ms ease, transform 200ms ease, visibility 0s linear 200ms;
}

.trigger:hover + .popover {
  opacity: 1;
  transform: translateY(0px);
  visibility: visible;
  pointer-events: auto;
  transition: opacity 200ms ease, transform 200ms ease, visibility 0s linear 0s;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: ui-sans-serif, system-ui, sans-serif;
  position: relative;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 16px;
  max-width: 560px;
  min-height: 150px;
}

.trigger {
  display: inline-block;
  padding: 10px 12px;
  border: 2px solid #111;
  border-radius: 10px;
  background: #f2f2f2;
  font-weight: 700;
  cursor: default;
}

.popover {
  position: absolute;
  left: 16px;
  top: 64px;
  width: min(360px, calc(100% - 32px));
  padding: 12px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #fff;
  opacity: 0;
  transform: translateY(-8px);
  visibility: hidden;
  pointer-events: none;
}

.popover p {
  margin: 0;
}
Hover me

I animate in/out with opacity and transform.

Notice the trick: visibility waits until the fade finishes (that 0s linear 200ms part), so it doesn’t become “hidden” until after the opacity animation.

Learn more about CSS transitions in the CSS Transition Interactive Tutorial.

Using display: none only at the end

In real apps, a common approach is:

  1. Start hidden: display: none.
  2. When opening: set display to something (like block), then animate opacity/transform.
  3. When closing: animate opacity/transform down to hidden, then set display: none.

Step 3 requires either JavaScript (to wait for transition end) or a CSS-only approach that never truly uses display: none during the animation. If you want pure CSS and smooth visuals, prefer the “no display” approach shown above.

CSS display: none Not Working

When people say “display: none is not working”, it’s almost always one of these:

1) Specificity or order: your rule is getting overridden

If another rule later in the stylesheet (or a more specific selector) sets display back to something else, your display: none won’t win.

.notice {
  display: none;
}
.notice {
  display: none;
}

.container .notice {
  display: block;
}
.container .notice {
  display: none;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.container {
  font-family: ui-sans-serif, system-ui, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 14px;
  max-width: 560px;
}

.notice {
  margin-top: 10px;
  padding: 12px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #f2f2f2;
}
Specificity demo
I might be hidden… or not. Depends on the snippet.

If you’re debugging in DevTools, look at the “Computed” styles for display and you’ll see which rule is winning.

Learn more about CSS specificity in the CSS Specificity Interactive Tutorial.

2) It is hidden, but you’re looking at the wrong element

Example: you hide a child, but the parent still shows an empty border/background, so it feels “not hidden”. Or you hide the parent but you’re inspecting the child.

3) Inline styles or !important

If an element has something like style="display: block", that inline style usually wins over your stylesheet. And !important rules can also complicate things.

.box {
  display: none;
}
.box {
  display: none !important;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.box {
  font-family: ui-sans-serif, system-ui, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 14px;
  background: #f2f2f2;
  max-width: 560px;
}
I have an inline display: block (Switch to the HTML tab to view). Snippet 1 may lose.

Rule of thumb: prefer fixing the selector/order rather than reaching for !important, but it’s good to know it exists when you truly cannot change the source.

Learn more about !important in the CSS !important Interactive Tutorial.

CSS display: none Opposite

The “opposite” of display: none depends on what the element should be when visible:

  • Text-like elements: display: inline or inline-block
  • Section containers: display: block
  • Flex layouts: display: flex
  • Grid layouts: display: grid

If you don’t know what it should be, a safe choice for most containers is display: block, but not always.

.box {
  display: none;
}
.box {
  display: block;
}
.box {
  display: flex;
  gap: 8px;
}
.box {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 8px;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 14px;
  max-width: 560px;
}

.box {
  margin-top: 10px;
  padding: 10px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #f2f2f2;
}

.pill {
  padding: 8px 10px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #fff;
  font-weight: 700;
  text-align: center;
}
Show / layout modes
One
Two
Three

Learn more about display: block; in the CSS Display: Block Interactive Tutorial, about display: flex; in the CSS Flexbox Interactive Tutorial, and about display: grid; in the CSS Grid Interactive Tutorial.

CSS display: none If Empty

Sometimes you want to hide an element only when it has no content (like an empty label, empty alert, or empty container).

Using :empty (works only if it is truly empty)

:empty matches elements that have no child elements and no text nodes. Important gotcha: whitespace counts as text.

  • <div class="msg"></div> is empty.
  • <div class="msg"> </div> is not empty (it has a whitespace text node).
.msg:empty {
  display: block;
}
.msg:empty {
  display: none;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: ui-sans-serif, system-ui, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 14px;
  max-width: 560px;
  display: grid;
  gap: 10px;
}

.msg {
  padding: 12px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #f2f2f2;
}

.label {
  font-weight: 700;
}
This one is empty (should hide):
This one has text (should show):
Hello, I have content.

What CSS cannot detect

CSS can’t do “if this contains only whitespace” or “if this contains only an empty child somewhere deep” in a reliable way across browsers. If your “empty” meaning is more complex than :empty, you typically solve it with:

  • Cleaning the HTML (don’t render the element if there’s no content), or
  • JavaScript adding a class like .is-empty.

CSS display: none Block

This phrase usually shows up in two situations:

  • You want to hide something (display: none), then show it as a block later (display: block).
  • You’re confused because display: none “overrides” your display: block elsewhere (specificity/order issue).

A clean toggle pattern with a class

Default visible, then hide when a class is present:

.toast {
  display: block;
}
.toast.is-closed {
  display: none;
}
.toast.is-closed {
  display: none;
}

.toast {
  display: block;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.toast {
  font-family: ui-sans-serif, system-ui, sans-serif;
  max-width: 520px;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 14px;
  background: #f2f2f2;
}

.toast strong {
  display: inline-block;
  margin-bottom: 6px;
}
Saved! Your settings were updated.

If the toast should be flex or grid when visible, make that the default:

  • .toast { display: flex; } and .toast.is-closed { display: none; }

CSS display: none VS hidden (Quick Recap)

  • display: none is a CSS rule you write in a stylesheet.
  • hidden is an HTML attribute that browsers treat like display: none by default.
  • Both remove the element from layout and interaction.

CSS display: none vs visibility: hidden Summary

  • display: none: no layout space, no visibility, no interaction.
  • visibility: hidden: keeps layout space, no visibility, no interaction.
  • opacity: 0: keeps layout space, invisible, but still interactive unless you disable it.

Animation Patterns That Replace display: none

When you want something to “feel like” it’s appearing/disappearing, you typically pick one of these patterns:

1) Fade only: opacity

Good for: tooltips, subtle UI, overlays. Add pointer-events: none when hidden.

.fade {
  opacity: 0;
  pointer-events: none;
  transition: opacity 250ms ease;
}

.wrap:hover .fade {
  opacity: 1;
  pointer-events: auto;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 14px;
  max-width: 520px;
}

.fade {
  margin-top: 12px;
  padding: 12px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #f2f2f2;
}

.hint {
  margin-top: 10px;
  font-size: 14px;
  opacity: 0.85;
}
Fade demo
I fade in and out.

Hover this box to fade the panel in.

2) Slide + fade: transform and opacity

Good for: dropdowns, popovers. Avoid animating layout when you can (transforms are usually smooth).

.dropdown {
  opacity: 0;
  transform: translateY(-10px);
  pointer-events: none;
  transition: opacity 200ms ease, transform 200ms ease;
}

.wrap:hover .dropdown {
  opacity: 1;
  transform: translateY(0px);
  pointer-events: auto;
}
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: ui-sans-serif, system-ui, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 14px;
  max-width: 520px;
  position: relative;
  min-height: 160px;
}

.dropdown {
  margin-top: 12px;
  padding: 12px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #f2f2f2;
  width: min(320px, 100%);
}

.hint {
  margin-top: 10px;
  font-size: 14px;
  opacity: 0.85;
}
Dropdown

Hover this box to open the dropdown.

3) Collapse: max-height + overflow: hidden

Good for: accordions with roughly known heights. Not perfect for unknown content size, but very common.

You already saw this pattern earlier, and it’s worth repeating because it shows up everywhere.

Accessibility Notes

  • display: none removes content from screen readers too. That’s usually correct for “closed” UI.
  • If something becomes visible, make sure keyboard users can reach it (and it’s not hidden via opacity but still focusable).
  • If you’re doing motion-heavy UI, respect users who prefer less animation with @media (prefers-reduced-motion: reduce).
@media (prefers-reduced-motion: reduce) {
  .panel {
    transition: none;
  }
}
*,
::before,
::after {
  box-sizing: border-box;
}

.panel {
  font-family: ui-sans-serif, system-ui, sans-serif;
  max-width: 520px;
  border: 2px solid #111;
  border-radius: 12px;
  margin: 30px;
  padding: 14px;
  background: #f2f2f2;
  transition: transform 250ms ease;
}

.panel:hover {
  transform: translateY(-6px);
}
Hover me. If the user prefers reduced motion, the transition should be removed.

A Quick Debug Checklist for display: none

  • Is the selector matching? (Check DevTools: does your rule appear?)
  • Is another rule overriding it? (Specificity or later order.)
  • Inline style? (Look for style="display: ...".)
  • Are you hiding the right element? (Parent vs child confusion.)
  • Are you confusing display with opacity? (Opacity hides visually but can still intercept clicks.)

Final Takeaways

  • display: none removes an element from layout and interaction.
  • visibility: hidden hides it but keeps its space.
  • The HTML hidden attribute behaves like display: none by default.
  • You can’t transition or animate display, but you can animate opacity, transform, max-height, and use visibility smartly.
  • For “hide if empty”, :empty works only when the element is truly empty (no whitespace).