I build backends. On the backend, I decide what runs first. The order is mine to write, mine to read, mine to debug.
CSS is the one place in the stack where I don't get to decide. The browser decides which rule wins, and it decides by a precedence table I can read but not rewrite. That is the whole thing to understand about CSS. It is not a styling language with quirks. It is a rules engine, and the surprises only look like surprises until you read the table.
Here is the table, in the order the browser applies it. When two rules disagree about the same element, specificity is the score. An ID selector beats a class selector beats an element selector, no matter where each one sits in the file:
p { color: black; }
.card p { color: blue; }
#main p { color: red; }
The paragraph is red. Not because the ID rule came last in the file — it could come first and still win. Specificity outranks position. Source order only breaks ties: when two rules have the same specificity, the one that appears later wins. That is the entire conflict-resolution system, and it is deterministic. The browser is never guessing.
Then there is the escape hatch: !important. It overrides specificity entirely, and it is the one part of the system I treat as a warning sign. It works, and it breaks the system's ability to reason about itself. Once an !important is in the stylesheet, the precedence table has a hole in it — the next person reading the file can't tell which rule wins by reading the table anymore. They have to search for overrides. It is the same failure shape I keep writing about on the backend: the override is invisible to the next reader, so the system fails silently instead of loudly.
Inheritance is the other half. Some properties flow down to children unless something closer overrides them — color, font. Others don't — margin, padding. It reads like defaults in a config file: the value applies until something nearer says otherwise. Env beats file beats default, parent beats nothing, the closest rule wins. Same shape, different syntax.
That is why CSS keeps pulling me back even though I live on the backend. Every backend system I build has its own version of this table. Middleware ordering decides which handler sees the request first. Config precedence decides which value the service actually runs with. Retry priority decides which attempt goes first when everything is failing at once. In each case, the system has a deterministic answer for which rule wins, and in each case the bug is almost never in the table. The bug is in my model of the table — I assumed order meant one thing and it meant another.
So when a style doesn't apply and I can't see why, I don't reach for a bigger hammer. I ask which rule has the higher score, whether the tie was broken by position, and whether something upstream already claimed the win with an override I forgot. The answer is always in the table. It was just in a row I wasn't looking at.
CSS is a rules engine with a precedence table. Once you read it that way, the surprises stop being surprises.
I write about backend systems and the decisions hiding inside them. Follow along. Next in this series: Integrating data governance in agile projects.
Top comments (1)
Viewing CSS through the lens of a deterministic rules engine rather than "whimsical styling magic" is such a refreshing and grounding perspective 🎯. Your parallel between CSS specificity vs. source order and backend middleware ordering or configuration cascades (env > file > default) nails why so many backend devs feel disoriented: the mental friction isn't that CSS is arbitrary, it's just that the browser's precedence table is evaluated by a scoring matrix rather than sequential execution.
To push that backend analogy even further, modern CSS finally gave us
@layer(Cascade Layers), which essentially lets us declare explicit execution tiers before selectors even get scored—giving architecture precedence over raw selector specificity 🛠️. Given your focus on keeping precedence tables legible and avoiding the silent failure modes of!important, have you started leveraging@layerto structure stylesheets into explicit boundaries (like reset < themes < components), or do you find that team discipline around flat specificity scores is still the cleaner operational baseline for you?Reaching for
!importantreally is the frontend equivalent of dropping a blanketexcept Exception: passinside your root middleware—sure, the 500 error disappears from your dashboard today, but someone downstream is going to spend their entire weekend questioning reality 😅.