Skip to content

How CSS Works

beginner11 min read

Your Styles Don't Apply the Way You Think

You write color: red and the text turns red. Feels like magic, right? But between your declaration and the final pixel color, the browser runs a multi-stage pipeline: parsing, tokenizing, building a tree, resolving conflicts across multiple origins, calculating specificity, handling inheritance, converting relative values to absolute ones, and finally resolving layout-dependent values during rendering.

Here's the thing -- understanding this pipeline is the difference between debugging CSS by trial and error and debugging it by knowing which stage went wrong.

Mental Model

Think of CSS processing as a courtroom trial. Multiple witnesses (stylesheets) present conflicting testimony (declarations). The judge (cascade algorithm) has strict rules for which testimony wins: first check the court (origin), then the credibility (specificity), then the order they spoke (source order). The verdict (computed value) is final — but the sentence (used value) depends on context the judge didn't have at trial time.

Stage 1: Parsing CSS Into the CSSOM

So what actually happens when the browser encounters a <link> tag or <style> block? It hands the raw bytes to the CSS parser. The parser:

  1. Decodes bytes into characters using the specified encoding (UTF-8 by default)
  2. Tokenizes characters into CSS tokens — identifiers, strings, numbers, delimiters
  3. Parses tokens into a tree of rules, selectors, and declarations
  4. Builds the CSSOM (CSS Object Model) — a tree structure mirroring the DOM
/* This CSS text... */
body {
  font-size: 16px;
  color: #333;
}

.header {
  font-size: 2rem;
}

/* ...becomes a CSSOM tree:
   StyleSheet
   ├── CSSStyleRule (selector: "body")
   │   ├── font-size: 16px
   │   └── color: #333
   └── CSSStyleRule (selector: ".header")
       └── font-size: 2rem
*/

CSS parsing is forgiving. Unlike JavaScript, an invalid CSS declaration doesn't crash the parser. The browser silently discards what it can't understand and keeps going:

.box {
  color: red;
  font-size: ????; /* Invalid — browser ignores this line */
  background: blue; /* This still applies */
}

This error recovery behavior is why CSS feature detection works — old browsers ignore display: grid because they can't parse it, and the fallback display: block still applies.

Common Trap

CSS parsing is render-blocking. The browser won't paint anything until it has built the CSSOM because it needs styles to compute the render tree. A massive unoptimized CSS file delays your first paint — even if most rules don't apply to the current page.

Stage 2: The Cascade Algorithm

This is where it gets interesting. When multiple declarations target the same property on the same element, the cascade resolves the conflict. The algorithm checks — in order:

1. Origin and Importance

CSS has three origins, checked in this priority order (highest to lowest for normal declarations):

PriorityOriginExample
1User agent !importantBrowser default !important rules
2User !importantUser stylesheet !important
3Author !importantYour !important declarations
4Animation@keyframes values during animation
5Author normalYour regular stylesheets
6User normalUser preferences
7User agent normalBrowser defaults (<h1> is bold)

Now here's the part that surprises everyone: for !important declarations, the priority order reverses. User agent !important beats author !important. This protects accessibility — a user's high-contrast stylesheet can override your design.

2. Specificity

If two declarations have the same origin and importance, specificity breaks the tie. Specificity is calculated as a three-component tuple (ID, CLASS, ELEMENT):

/* (0, 0, 1) — one element selector */
p { color: blue; }

/* (0, 1, 0) — one class selector */
.text { color: green; }

/* (1, 0, 0) — one ID selector */
#intro { color: red; }

/* (0, 1, 1) — one class + one element */
p.text { color: purple; }

/* (1, 1, 1) — one ID + one class + one element */
p#intro.text { color: orange; }
Specificity is not base-10

Specificity is often taught as a single number (e.g., "an ID is worth 100 points"). This is misleading. Specificity is compared component by component, left to right. A selector with 11 classes (0, 11, 0) does NOT beat one ID (1, 0, 0). The ID column wins before classes are even considered.

3. Source Order

If origin and specificity are equal, the declaration that appears last in source order wins.

.box { color: red; }
.box { color: blue; } /* Wins — same specificity, later source order */
Execution Trace
Collect
Gather all declarations for 'color' on this element
From user-agent, user, author stylesheets
Origin
Filter by origin priority
Author normal beats user-agent normal
Specificity
Compare specificity tuples
(1,0,0) beats (0,11,0) — IDs win over any number of classes
Order
Last declaration wins
Only reached if origin AND specificity are identical
Computed
Winning value becomes the computed value
Relative units resolved against parent/root

Stage 3: Computed, Used, and Resolved Values

You'd think we're done after the cascade picks a winner. Not quite. The value goes through several more transformations:

Specified Value

The value declared in CSS after cascade resolution, or the inherited/initial value if no declaration exists.

Computed Value

Relative values resolved as far as possible without layout:

  • em converted relative to parent font-size
  • rem converted relative to root font-size
  • inherit resolved to parent's computed value
  • Relative URLs made absolute
/* If root font-size is 16px and parent font-size is 20px */
.child {
  font-size: 1.5em;    /* Computed: 30px (1.5 × 20px parent) */
  width: 50%;          /* Computed: 50% (can't resolve without layout) */
  margin: 2rem;        /* Computed: 32px (2 × 16px root) */
}

Used Value

The value after layout. Percentages that depend on containing block dimensions are resolved here:

.child {
  width: 50%; /* Used: 400px (if containing block is 800px wide) */
}

Actual Value

The final value after rounding and device constraints. If the device can't render 14.7px borders, the actual value might be 15px.

Info

When you inspect an element in DevTools, the "Computed" tab shows resolved values — a mix of computed and used values designed to be useful for debugging. For layout-dependent properties like width, you see the used value. For things like color, you see the computed value.

Production Scenario: "Why Doesn't My Override Work?"

/* library.css (loaded first) */
.btn.btn-primary { /* Specificity: (0, 2, 0) */
  background: #007bff;
}

/* app.css (loaded second) */
.custom-btn { /* Specificity: (0, 1, 0) */
  background: #ff6600; /* Doesn't win — lower specificity */
}

The fix is not !important. Resist that urge. The real fix is understanding the cascade:

/* Option 1: Match or exceed specificity */
.btn.custom-btn {
  background: #ff6600; /* (0, 2, 0) — ties, but later source order wins */
}

/* Option 2: Use a cascade layer (modern approach) */
@layer library, app;

@layer library {
  .btn.btn-primary { background: #007bff; }
}

@layer app {
  .custom-btn { background: #ff6600; } /* Layer order wins over specificity */
}
What developers doWhat they should do
Thinking specificity is a single number (100 for ID, 10 for class, 1 for element)
11 classes (0,11,0) still loses to 1 ID (1,0,0)
Specificity is a three-component tuple compared column by column — no amount of classes can outweigh one ID
Using !important to win specificity battles
!important creates a parallel specificity war that's even harder to override
Restructure selectors, use cascade layers, or increase specificity minimally
Thinking CSS loads like JavaScript — non-blocking
Without styles, the browser can't compute the render tree
CSS is render-blocking — the browser delays painting until the CSSOM is built
Assuming computed values are the same as used values
width: 50% can't become pixels until the containing block size is known
Computed values resolve what's possible without layout. Used values resolve after layout.
Quiz
If an element is targeted by both .card .title (specificity 0,2,0) and #main p (specificity 1,0,1), which wins?
Quiz
What is the computed value of width: 50% on an element whose containing block is 800px wide?
Quiz
A library uses .btn.btn-primary { color: red; } and you write .my-btn { color: blue; } after it. What color is an element with all three classes?
Key Rules
  1. 1CSS is render-blocking — large stylesheets delay first paint
  2. 2The cascade resolves conflicts by: origin > specificity > source order
  3. 3Specificity is a three-component tuple (ID, CLASS, ELEMENT) — never a single number
  4. 4Computed values resolve relative units without layout. Used values resolve after layout.
  5. 5CSS parsing is forgiving — invalid declarations are silently ignored, enabling progressive enhancement
1/11