CogniPrep is a Tailwind v4 app with a 719 line hand-written app/globals.css on top. That file contains 17 !important declarations, and most of them are load bearing. Every one of the four incidents below came from it, and three of the four were invisible in the component that broke.
If you have a global stylesheet next to a utility framework, this post is about the two ways that file can win a fight you did not know it was in.
1. An unlayered rule beats every utility, at any specificity
Tailwind v4 emits its utilities inside cascade layers. You can see it in the built CSS: @layer base, then @layer components, then @layer utilities. Layers change the cascade before specificity is considered, and the rule people forget is the one that matters most here:
Unlayered CSS wins against every layered rule, no matter how specific the layered rule is.
So this, written at the top level of globals.css, outranks border-transparent, border-destructive, hover:border-primary and every other border colour class in the app:
* {
border-color: var(--border);
}
A * selector has specificity zero. It still wins, because it is not in a layer and the utility is. The comment now in our source records what that cost at the time:
While this sat unlayered it silently defeated all 514 border-colour utilities
in the app, so border-transparent, border-destructive, border-teal-500,
hover:border-primary and the rest all rendered as this one grey.
A grep over the repo today finds about 800 border colour utilities, so the blast radius has only grown. The visible casualty was not a subtle shade, though. It was a CSS triangle: an element with transparent left and right borders and a coloured top border, which is the old trick for drawing a triangle with no SVG. Force its transparent borders to grey and the triangle renders as a solid rectangle.
The fix is two lines of nesting:
@layer base {
* {
border-color: var(--border);
}
}
Now it is a default that any utility can override, which is what it was always meant to be.
2. The same bug, in a place where it cut content off the screen
The second one was worse, because it broke layout rather than colour:
main {
min-height: calc(100dvh - 3.5rem);
contain: layout style paint;
}
Reasonable intent: an ordinary page's <main> sits under a 3.5rem header and should fill the viewport so the footer does not jump during loading. Unlayered, it also beat every min-h-* class in the app, including the min-h-0 flex-1 that full-screen shells use for their scroll area inside a 100dvh box. Those shells came out about 8 pixels taller than the viewport, and on a phone that is enough to push a screen's primary button under the bottom edge with no way to scroll to it.
Same fix, same two lines. And the comment above it now explains both the layer and why the box is 100dvh rather than 100vh, because on mobile 100vh is the viewport you only get once the browser chrome is hidden.
3. A substring selector matches more than you think
This one is my favourite, because the selector reads as obviously correct:
[class*='border-t'] {
border-top: none !important;
border-top-width: 0 !important;
}
It was there to suppress a divider line on sections that opt into one. [class*=...] matches any class containing that string, and Tailwind class names contain a lot of strings:
-
border-teal-500containsborder-t. Circles drawn with a teal ring lost their top edge. -
border-t-24containsborder-t. That was the triangle from incident 1, rebuilt with an explicit width, and removing its top border removes the entire shape, so it vanished.
The rule is now an exact class selector, with a comment that tells the next person why it must stay one:
/*
* This MUST stay an exact class selector. Colour and width variants such as
* border-t-foreground are drawing something specific and must not be caught
* by a rule aimed at dividers.
*/
.border-t {
border-top: none !important;
border-top-width: 0 !important;
}
We kept exactly one substring selector, [class*='bg-card'], and only because Tailwind writes opacity modifiers into the class name itself, so bg-card/90 has to be caught too, and no other utility in the app contains the string bg-card. Its comment carries the consequence for anyone who tries to add a border to a card: that utility is silently dead, use a tinted surface instead.
4. And the trap that survives all of that
Narrowing the selector to .border-t fixed the collisions and left a live mine: any component that draws a one pixel rule with bare border-t gets nothing at all.
Last week it caught a component that stacks two numbers with a thin rule between them, written as border-foreground/80 w-full border-t. It rendered as two numbers sitting directly on top of each other. The fix was to ask for a width explicitly, because border-t-[1.5px] is a different class and the exact selector does not match it:
<span className="w-full border-t-[1.5px] border-current px-0.5 pt-[3px] text-center" />
That is the lasting cost of a global override. Not the bug it caused once, but the rule every future component has to know.
See it in the shipped CSS
Open any page on cogniprep.app, take the stylesheet from the <head> and search it for .border-t{. You get two hits in the same file:
/* inside @layer utilities, from Tailwind */
.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}
/* unlayered, later in the file, and this is the one that applies */
.border-t{border-top:0!important}
Search the same file for [class*=bg-card]{border:0!important} and for min-height:calc(100dvh - 3.5rem), which now sits inside an @layer base block. Then inspect any card in DevTools and watch a border utility on it get crossed out.
The practice that came out of it
Every !important rule in that file now carries a comment that names the incident it caused, not the intent it serves. Intent is guessable from the selector. The casualty is not, and the casualty is what stops the next person from reverting the fix.
Two checks worth stealing if you have a stylesheet like this one:
- Grep the file for top level rules. Anything not inside
@layeroutranks your entire utility framework. That is a deliberate choice or it is a bug, and it is almost always a bug. - Grep for
[class*=. For each hit, ask which Tailwind class names contain that substring, including colour and size variants and opacity modifiers. Then write the answer into the comment.
Top comments (0)