Building a dark admin theme for WordPress sounds like a color problem. In practice, most of the actual work is a specificity problem: a rule that correctly matches an element and still loses, because something else matches it too and wins the tiebreak. The same shape of this bug shipped three separate times in Mawu Dark Mode, once after the lesson had already been written down. Here’s what actually happened, and the CSS math that explains all three.
Specificity, the part that actually matters here
When two CSS rules target the same element for the same property, the browser doesn’t care which one appears later in the file. It computes a specificity score for each selector and the higher score wins, full stop. An ID selector like #plugin-information scores higher than any number of class or type selectors stacked together, an actual rule, not a rough guideline: one ID beats two, three, or ten classes every time. A rule that visibly exists in the page source, targets the right element, and still doesn’t apply almost always means something else outscored it, not that the rule is broken.
Case one: the post title
Version 0.2.2 shipped dark styling for the block editor’s writing canvas, and the post title stayed stubbornly in its default color. The dark CSS was scoped to .editor-styles-wrapper, the block editor’s own wrapper class for rendered content, which is where block content actually lives. The title field isn’t inside it. WordPress renders the post title as a separate input alongside the block content, not as part of it, so a selector scoped to the content wrapper had nothing to do with the title at all. This wasn’t really a specificity loss, it was a selector that never had a chance to match in the first place, but it set up the pattern for the two real specificity losses that followed: a rule that looks reasonable and turns out not to reach the thing it was meant to style.
Case two: the Tags input
Version 0.4.3 hit the real version of this bug. The block editor sidebar’s Tags panel renders its input via .components-form-token-field__input, a Gutenberg component class. The dark styling rule targeted that exact class and still lost, because Gutenberg’s own compiled CSS for that component carries more specificity in the same cascade. The selector was correct. It just wasn’t the only rule that mattered.
The fix was a single !important flag, and the lesson wasn’t really “add !important to this one input.” It was broader: anything reaching into a component system that generates its own deeply-nested or hashed class names should assume it’s fighting a specificity battle it might lose, and plan for that up front rather than discovering it after shipping.
Case three: the same mistake, after it was already documented
The third occurrence hit the Customizer’s accordion section titles, the panel headers like “Colors & Typography” in Appearance → Customize. By this point the lesson from case two was written down explicitly: default to !important for anything inside a framework’s own component tree. The rule that shipped for the Customizer titles didn’t follow it, a plain two-class selector against WordPress core’s own #customize-theme-controls .accordion-section-title, which is ID-qualified and outranks a two-class rule regardless of which file loads first or last.
Seven of eight sample section titles stayed white after the fix that was supposed to darken them, caught only by actually loading the real Customizer and checking computed styles, not by reading the CSS and confirming the rule was present. The rule being in the page source proved nothing about whether it was winning.
Why the same mistake shipped twice despite being documented
Writing down “use !important for component-tree selectors” didn’t stop the third case, because the actual trigger wasn’t Gutenberg specifically, it was any framework with its own deeply-nested, ID-qualified, or otherwise heavily-specific selectors. The original lesson was scoped to Gutenberg’s components- classes by name. The Customizer’s accordion system is a different framework entirely, built by a different part of WordPress core, with its own compound ID selectors that happen to share the exact same risk profile. A rule written narrowly enough (“watch out for Gutenberg specifically”) doesn’t automatically cover a structurally identical problem wearing different class names.
The generalized version, the one that actually holds going forward: any selector overriding output from a system that generates its own compound or ID-qualified selectors, whether that’s Gutenberg’s component library, the Customizer’s accordion, or some future admin framework nobody’s written CSS against yet, should default to !important rather than betting on winning a specificity fight it has no visibility into. The fix isn’t “remember this one framework.” It’s “assume the next unfamiliar framework has the same shape of risk until proven otherwise.”
Confirming a fix actually won, not just that it exists
All three cases share a second lesson, separate from the specificity math itself: a rule being present in a page’s rendered HTML source says nothing about whether it’s actually winning the cascade. The only way to know is to load the real page and check the computed style directly, the actual color the browser decided on after resolving every competing rule, not the CSS as written. Confirming a selector is correct and confirming a selector is winning are two different checks, and skipping the second one is exactly how a rule that looks finished ships broken.
See what Mawu covers in wp-admin for where all of this actually landed, or Mawu Dark Mode to see 13 themes that hold up against WordPress core’s own cascade, not just look right in isolation.
