I was cleaning up checkbox styling on a site I built: moving the styled-checkbox rules out of a page-specific CSS file and into a global component stylesheet, so they'd render consistently everywhere. Good idea — except on the /checkout page, where my custom checkbox state (an orange-filled box with a white check) still never appeared. What showed there was the native browser checkbox, the plain grey square.
The strange part: on every other page the styling was perfect. Only /checkout failed — exactly the page I least wanted to break.
The global component looked like this:
.checkbox input[type="checkbox"] {
position: absolute;
opacity: 0; /* hide the native input */
}
.checkbox .box {
width: 18px; height: 18px;
border: 1px solid #ccc;
}
.checkbox input:checked + .box {
background: #f97316; /* orange */
/* white check rendered here */
}That opacity: 0 is meant to hide the native input so the custom .box can stand in for it. On most pages it worked. On /checkout, the native input stayed visible — meaning my opacity: 0 was losing.
Root cause: an older, more specific selector still wins
I opened DevTools, clicked the native input, and looked at the Computed → Styles panel. It was clear: my opacity: 0 rule was struck through, and one older rule was winning over it:
.checkout-page .checkbox input[type="checkbox"] {
opacity: 1; /* ← the winner */
position: static;
}This rule was a leftover from the checkbox styles' old home — it used to live in the checkout page-specific file, deliberately forcing the native input to stay visible. I moved the new styles into the global stylesheet but forgot to delete this page-scoped override.
And here's the crux — specificity. Score both selectors:
.checkbox input[type="checkbox"] → (0,2,1) = 1 class + 1 attr + 1 element
.checkout-page .checkbox input[type="checkbox"] → (0,3,1) = 2 classes + 1 attr + 1 element
The old selector carries the extra .checkout-page ancestor, so its specificity is higher. In the cascade, highest specificity wins — regardless of which stylesheet loaded later. So the old opacity: 1 beat my global opacity: 0, the native input was never hidden, and the custom state had no room to render.
The deceptive part: my global rule was loaded later in the document. The common instinct is "last one wins," but source order only breaks ties when specificity is equal. Here it wasn't equal — so .checkout-page won despite being defined first.
How to trace it in DevTools
This is the pattern, so you can recognize it in seconds instead of hours:
- Inspect the wrong element (here, the native input that's showing).
- Open the Computed tab, find the misbehaving property (
opacity), click the arrow to expand it. - DevTools lists all contributing rules from winner to loser. The winner is at the top; the losers are struck through.
- Look at the top non-struck rule — that's what's winning. Here:
.checkout-page .checkbox input[type="checkbox"]. - A selector with an extra ancestor (
.checkout-page) is almost always the suspect in a "why only on this page" bug.
The moment you see your global rule struck through while a longer ancestor-prefixed rule wins, the diagnosis is done: this is a specificity war, not a loading-order problem.
The fix: delete the old page-scoped override
No !important, no bumping the global rule's specificity (which just spreads the problem). The correct fix is to delete the page-scoped selector that used to compete with the rule in its old location:
/* DELETE from the checkout file — relic from the old home */
.checkout-page .checkbox input[type="checkbox"] {
opacity: 1;
position: static;
}Once that override is gone, the global .checkbox rules finally win on /checkout like they do everywhere else. The native input hides, and the orange box with the white check finally renders.
Lesson
When you promote a component's CSS from a page-specific file to a "global" stylesheet, don't just move the new rules — also hunt down any page-scoped selectors that previously competed with them. Those selectors often carry extra ancestors (.checkout-page, .single-product, and so on) that give them higher specificity, so they keep winning on exactly the page you care about. Loading order won't save you — specificity decides. Open the Computed panel, see which rule wins and which are struck through, and delete the now-useless override rather than piling !important on top of it.
