The hidden attribute stops working when CSS sets display
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 had hidden and it made no difference.
What was happening
The hidden attribute works through a single rule in the user-agent stylesheet:
[hidden] { display: none; }
That rule sits at the bottom of the cascade. The moment any author rule sets display on the same element, the author rule wins, because author styles beat user-agent styles, and hidden is silently ignored. My .palette-backdrop { display: flex } is a plain single-class selector, and it’s still an author rule, so it overrode [hidden] and the element rendered display: flex whether the attribute was there or not.
The second-order bite is that once CSS rather than 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 has 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;
}
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
Every layer looks right. The HTML has hidden, the JS toggles hidden, and 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 one 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 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.
Discussion
Powered by GitHub. Sign in to leave a comment.