The hidden attribute stops working when CSS sets display

2 min read CSSHTMLAccessibility

A modal with the hidden attribute rendered open on every page load and refused to close. The cause: a one-line CSS display rule silently overrides the hidden attribute, and a one-line attribute selector fixes it.

TL;DR · THE FIX

Setting display in CSS overrides the hidden attribute, because an author rule beats the user-agent [hidden] { display: none } rule. The element never hides and toggling el.hidden in JS does nothing. Re-assert it next to your rule with your-selector[hidden] { display: none }.

The symptom

I built a Ctrl+K command palette as a modal: a backdrop div that’s hidden until you open it.

<div id="palette" class="palette-backdrop" hidden>
  <!-- search input + results -->
</div>
.palette-backdrop {
  position: fixed;
  inset: 0;
  display: flex;          /* center the dialog when shown */
  align-items: flex-start;
  justify-content: center;
}

The JavaScript opened it with el.hidden = false and closed it with el.hidden = true. Every piece read as correct. Yet the palette was open on every page load, covering the whole site, and nothing would dismiss it. The markup clearly had hidden. It made no difference.

What was actually happening

The hidden attribute isn’t special-cased by the browser. It works through a single rule in the user-agent stylesheet:

[hidden] { display: none; }

That user-agent rule sits at the bottom of the cascade. The moment any author rule sets display on the same element, the author rule wins (author styles beat user-agent styles), and hidden is silently ignored. My .palette-backdrop { display: flex } is a plain single-class selector, but it’s still an author rule, so it overrode [hidden]. The element rendered display: flex whether the attribute was there or not.

And here’s the second-order bite: once CSS, not the attribute, decides visibility, toggling el.hidden in JavaScript does nothing visible. The “close” handler dutifully set hidden, the CSS kept showing it, and the modal sat there uncloseable.

The fix

Re-assert the hidden state with a selector that outranks the bare class. A class plus an attribute selector is specificity (0,2,0), which beats .palette-backdrop at (0,1,0), so no !important is needed:

.palette-backdrop {
  display: flex;
  /* ... */
}

/* make `hidden` win again */
.palette-backdrop[hidden] {
  display: none;
}

That’s the whole fix. Now the attribute controls visibility the way it should: hidden on load, el.hidden = false shows it, el.hidden = true hides it again.

Why it slipped through

This one is sneaky because every layer looks right. The HTML has hidden. The JS toggles hidden. It even survives a “fetch the page and check the element is there” test, because the markup is correct and only the rendered result is wrong. The single thing that catches it is loading the page in a real browser (or a headless one in CI) and asserting the element is genuinely not visible. A static HTML check waves it straight through.

The lesson

Any element you show and hide via the hidden attribute must not have its display set by an unconditional CSS rule, or the attribute quietly stops working. Modal backdrops, flex and grid wrappers, and dropdowns almost always set display, so they’re the usual victims. Whenever you write display on such an element, add the guard right beside it:

.thing { display: flex; }
.thing[hidden] { display: none; }

The same trap applies to framework [hidden] bindings and to anything else whose visibility you drive by attribute.

Related fixes

Discussion

Powered by GitHub. Sign in to leave a comment.