The symptom had me scratching my head for a solid half hour. On a client's custom theme there was a filter panel that was supposed to appear when a button was clicked. The logic was trivial: add an .is-open class to the panel, and in CSS .is-open is display: block. But the panel flat-out refused to show. No console error, no warning, nothing. The class was clearly attached, yet the element stayed invisible.
The markup looked roughly like this:
<div class="filter-panel" hidden>
<!-- filter contents -->
</div>And the CSS I assumed was enough:
.filter-panel.is-open {
display: block;
}The JavaScript was just adding and removing the class:
button.addEventListener('click', () => {
panel.classList.toggle('is-open');
});I opened DevTools and inspected the element. The is-open class was there. But in the Computed panel, display was still none. I tried bumping specificity, slapping on !important, and everything felt dirtier while still being inconsistent. Something was pinning this element shut, and it wasn't my CSS.
Why this happens
The culprit was that hidden attribute sitting in the markup. hidden is not a cosmetic attribute — it is a strong signal from HTML that the element is "not currently relevant" and should not be rendered. Browsers enforce it through the user-agent stylesheet with this rule:
[hidden] {
display: none;
}So two display declarations were applying to the same element: display: none from the UA stylesheet (because of the hidden attribute) and display: block from my .is-open class. What tripped me up is that, on paper, a class's specificity should win over the UA rule. In practice, relying on a class to override [hidden] is brittle. The semantic intent of hidden is that the element stays hidden, and cranking up specificity to force an element you deliberately marked as not-relevant to appear is fighting against the grain of HTML's own design.
The short version: I was treating hidden as an ordinary styling hook, when it is really a browser-level display: none with strong intent behind it. As long as that attribute stays on the element, I'd keep waging a dirty, unreliable war against the UA stylesheet.
The fix
The answer wasn't more CSS — it was to stop fighting and manage the attribute directly. If the element carries the meaning of hidden, then pull the attribute off when you want it shown, and put it back when you want it hidden. This is the clean, semantic approach:
button.addEventListener('click', () => {
if (panel.hasAttribute('hidden')) {
panel.removeAttribute('hidden');
} else {
panel.setAttribute('hidden', '');
}
});To reveal, panel.removeAttribute('hidden'). To hide, panel.setAttribute('hidden', ''). Once the hidden attribute is gone, the UA's [hidden] { display: none } rule no longer applies, and the element falls back to its default display behavior without needing any class at all. No more specificity war, no !important, and as a bonus the visibility state is now correct for accessibility too, because hidden is a signal assistive technology actually understands.
If you genuinely have a reason to keep driving visibility purely through CSS — say the panel is controlled by some other class state outside this particular JS — there is an escape hatch, but you have to explicitly neutralize the UA stylesheet first:
[hidden] {
display: revert;
}
.filter-panel {
display: none;
}
.filter-panel.is-open {
display: block;
}With [hidden] { display: revert }, you deliberately remove the attribute's forced display: none, then let your own class drive visibility entirely. Honestly though, that is more moving parts and it throws away the semantic meaning of hidden. For my case, removing and adding the attribute in JS was far cleaner, so that is what I shipped.
The takeaway
hidden is not a styling hook. It is a browser-level display: none with strong intent: "this element is not relevant right now." Once that clicks, the debugging gets clear — you are not fighting your own CSS, you are fighting HTML's intent. Don't try to out-CSS the hidden attribute by raising specificity or piling on !important; it is brittle and it works against the semantics. Toggle the attribute instead: removeAttribute('hidden') to show, setAttribute('hidden', '') to hide. Less code, more semantic, and accessibility comes for free. Now, whenever an element refuses to appear even though my class is clearly correct, the first thing I check is whether a hidden attribute is quietly sitting in the markup.
