CSS Module 1 – How CSS works
How browsers apply CSS to a page
How a browser parses CSS, matches selectors, runs the cascade and draws boxes, and why one broken declaration leaves the rest of its rule working.
What you will learn
- Describe how a browser turns HTML and CSS into styled, laid-out boxes on screen
- Identify the parts of a rule, such as selector, declaration block, property, value and at-rule
- Explain why an invalid declaration is dropped while the rest of the rule still applies
What you’ll learn
What you will learn
- Describe how a browser turns HTML and CSS into styled, laid-out boxes on screen
- Identify the parts of a rule, such as selector, declaration block, property, value and at-rule
- Explain why an invalid declaration is dropped while the rest of the rule still applies
On this page
A browser applies CSS in a fixed series of steps. It reads the HTML into a tree of elements, the DOM, and every
stylesheet into a list of rules, the CSSOM. For each element it finds the rules whose selectors match, lets the
cascade choose one winning value for every property, and turns those values into absolute ones, so 2.5rem
becomes 40px. Once every value is known, it works out the size and position of each box (layout), draws the
boxes (paint) and puts the drawn layers together on screen (composite).
On the way in, the CSS parser is forgiving. A declaration it cannot read is dropped without any error message, and the rest of its rule still applies. That makes a page hard to break, and it also lets a typo go unnoticed for months. This lesson follows each step, names the parts of a rule, and runs a stylesheet with four planted mistakes to show exactly what a browser throws away.
From bytes to pixels
Text description of the diagram
The diagram shows the stages a browser goes through, from top to bottom.
- The HTML bytes are parsed into the DOM, the tree of elements.
- The CSS bytes are parsed into the CSSOM, the style sheets and their rules. The parser drops whatever it cannot read.
- Style combines the two: the browser matches selectors against elements, runs the cascade and computes one value for every property of every element.
- Layout gives every box a size and a position.
- Paint draws the boxes as pixels, in layers.
- Composite combines the layers into what appears on the screen.
From bytes to pixels
Each stage in the diagram works on what the stage before it produced:
- Parse. The HTML parser builds the DOM. Every piece of CSS, whether it comes from a file, a
<style>element or astyleattribute, goes through the CSS parser, which builds the CSSOM: the style sheets, the rules in them and the declarations in each rule. JavaScript can read and change both trees. - Style. For each element, the browser collects the declarations of every rule whose selector matches it. When
several declarations set the same property, the cascade picks the winner with a fixed list of tests: where the
declaration comes from and whether it is
!important, whether it sits in astyleattribute, its cascade layer, its specificity and finally its position in the source. A property that nothing sets is inherited from the parent element or takes its initial value. The result is one computed value for every property of every element. - Layout. Elements become boxes, and each box gets a size and a position. The
displayandpositionvalues computed in the previous step decide which layout rules apply: normal flow, flexbox, grid or positioning. - Paint and composite. The boxes are drawn: backgrounds, borders, text and shadows. Some parts of the page are painted into layers of their own, and the browser then combines the layers into the picture on screen.
This order is how you diagnose a style that does not show up. A declaration that never reached the CSSOM was dropped by the parser. One that reached it but lost to another declaration was beaten in the cascade. One that won but changes nothing may not apply to that kind of box at all. The next lessons show how to tell these cases apart in the browser’s developer tools, and the selectors, cascade, box model and layout modules of this track each go deep into one stage.
Changes after the page first appears go through the same stages, but only from the first stage they affect. Changing
an element’s width means layout, paint and composite all run again. Changing its color or box-shadow skips
layout, because no box moves, so the browser only paints and composites.
The parts of a rule
Here is a short stylesheet that contains each part of CSS syntax this track talks about:
/* A comment: the parser skips everything between the two markers. */
/* A style rule: a selector, then a declaration block in braces. */
body {
font-family: system-ui, sans-serif;
line-height: 1.5;
}
/* A selector list: one rule for two kinds of heading. */
h1,
h2 {
color: #1d3557;
margin-block: 0 0.5em; /* a shorthand: margin-block-start and margin-block-end */
}
/* An at-rule: its rules apply only when the condition is true. */
@media (min-width: 40rem) {
body {
max-inline-size: 40rem;
margin-inline: auto;
}
} - A style rule, such as
body { … }, is a selector followed by a declaration block in braces. - The selector,
body, says which elements the rule applies to. - A selector list,
h1, h2, is several selectors separated by commas, sharing one block. - A declaration,
line-height: 1.5;, is a property, a colon and a value. Semicolons separate declarations. - The property,
line-height, is what the declaration sets, and the value,1.5, is what it sets it to. - An at-rule starts with
@. Here,@media (min-width: 40rem) { … }holds rules that apply only while its condition is true. - A comment,
/* … */, is text the parser skips. CSS has no other kind of comment.
Some at-rules hold a block, as @media does. Others are statements that end with a semicolon, such as @import,
which loads another stylesheet.
Some properties are shorthands that set several longhands at once. margin-block: 0 0.5em sets
margin-block-start to 0 and margin-block-end to 0.5em. A shorthand always sets every longhand it covers, and
the ones you leave out go back to their initial values. When Chromium 153, Firefox 155 and WebKit 26.6 read
background: #eef7f1, all three store it as nine longhand declarations: background-color plus eight others, such
as background-image, set to their initial values. So a background declaration written after a background-image
declaration removes the image.
What the parser throws away
The stylesheet below contains four mistakes of the kind that turn up in real projects. Try to spot all four before you read on.
h1 {
colr: #1d3557;
font-size: 2.5 rem;
margin-block: 0 0.5rem;
}
.note {
// a reminder for the team
color: #2a6f4e;
background: #eef7f1
padding: 0.75rem 1rem;
border-inline-start: 4px solid #2a6f4e;
} Browsers do not stop at the first error. CSS Syntax Module Level 3 describes how they recover, in two steps. Inside a declaration block, anything the parser cannot read as a declaration is thrown away up to the next semicolon, or to the end of the block, and the parser starts again after it. A declaration that does read correctly, but whose value does not fit its property’s grammar, is dropped on its own. The program below models those two steps for this file and for a corrected copy. It knows only the six properties the two files use, while a browser checks every property, so read it as an illustration of the rule rather than as a real parser.
// Which declarations survive in a stylesheet: a short model of the error recovery in CSS Syntax Module Level 3.
// A piece the parser cannot read as a declaration is thrown away up to the next semicolon, a declaration whose
// value does not fit its property's grammar is dropped on its own, and the rest of the rule still applies.
// The model reads plain style rules (no at-rules, no nesting) and knows only the properties and value types that
// broken.css and fixed.css use; a browser checks every property against its full grammar.
import { readFileSync } from "node:fs";
const hex = (v) => /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(v);
const length = (v) => /^(?:0|\d*\.?\d+(?:px|rem|em))$/.test(v);
const lengths = (min, max) => (v) => {
const parts = v.split(" ");
return parts.length >= min && parts.length <= max && parts.every(length);
};
const border = (v) => {
const parts = v.split(" ");
return parts.length <= 3 && parts.every((p) => length(p) || hex(p) || ["solid", "dashed", "dotted"].includes(p));
};
// The value grammar of each property the two files use. (background takes images and positions too;
// these files only give it a colour.)
const GRAMMAR = {
color: hex,
background: hex,
"font-size": length,
"margin-block": lengths(1, 2),
padding: lengths(1, 4),
"border-inline-start": border,
};
function verdict(piece) {
const declaration = /^([a-z-]+)\s*:\s*(.*)$/s.exec(piece);
if (!declaration) return "does not start with a property name and a colon";
const [, name, value] = declaration;
if (!Object.hasOwn(GRAMMAR, name)) return `unknown property "${name}"`;
if (!GRAMMAR[name](value)) return `not a valid ${name} value`;
return null; // kept
}
for (const file of ["broken.css", "fixed.css"]) {
// CSS has one kind of comment, /* ... */. Two slashes are ordinary characters.
const css = readFileSync(file, "utf8").replace(/\/\*[\s\S]*?\*\//g, " ");
let read = 0;
let kept = 0;
console.log(file);
for (const [, selector, block] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
console.log(`${selector.trim()} {`);
// Each piece runs up to the next semicolon, or to the end of the block.
const pieces = block.split(";").map((s) => s.replace(/\s+/g, " ").trim()).filter(Boolean);
for (const piece of pieces) {
read += 1;
const problem = verdict(piece);
if (problem === null) {
kept += 1;
console.log(` kept ${piece}`);
} else {
console.log(` dropped ${piece}`);
console.log(` (${problem})`);
}
}
console.log("}");
}
console.log(`kept ${kept} of the ${read} pieces the parser read\n`);
} Output
broken.css
h1 {
dropped colr: #1d3557
(unknown property "colr")
dropped font-size: 2.5 rem
(not a valid font-size value)
kept margin-block: 0 0.5rem
}
.note {
dropped // a reminder for the team color: #2a6f4e
(does not start with a property name and a colon)
dropped background: #eef7f1 padding: 0.75rem 1rem
(not a valid background value)
kept border-inline-start: 4px solid #2a6f4e
}
kept 2 of the 6 pieces the parser read
fixed.css
h1 {
kept color: #1d3557
kept font-size: 2.5rem
kept margin-block: 0 0.5rem
}
.note {
kept color: #2a6f4e
kept background: #eef7f1
kept padding: 0.75rem 1rem
kept border-inline-start: 4px solid #2a6f4e
}
kept 7 of the 7 pieces the parser read
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node recovery.mjs
Real browsers agree with the model. Loaded into Chromium 153, Firefox 155 and WebKit 26.6 (the engines that
Playwright 1.63.0 drives) and read back from the CSSOM, broken.css kept the same two declarations in all three:
h1: of the 3 declarations written, onlymargin-blockwas kept. The heading stayed in the default black, at the default 32px..note: of the 4 declarations written, onlyborder-inline-startwas kept. The note had black text, no background and no padding.
Here is what went wrong in each case:
colris not a property, so its declaration is dropped. Browsers skip unknown properties on purpose: a stylesheet written for a newer browser contains properties an older one has never heard of.2.5 remhas a space between the number and its unit. A length such as2.5remis written as one unit of syntax; with the space, the value is a number followed by a word, whichfont-sizedoes not accept.//does not start a comment in CSS. Where the parser expects a property name it finds two slash characters, so it throws away everything up to the next semicolon, and that includescolor: #2a6f4eon the line below. The model printed them as a single piece. The comment line did no harm on its own; it took the next declaration with it.- The missing semicolon after
background: #eef7f1joins two declarations into one:background, with the value#eef7f1 padding: 0.75rem 1rem, which no background can be. Both are lost. Only the last declaration in a block may leave out its semicolon, because the closing brace ends it, and writing one there anyway keeps the next declaration you add from being swallowed.
The page gets no error for any of this, and the console stays quiet unless you ask: Firefox, for example, lists CSS warnings only once its CSS filter is switched on. Dropped declarations do show up with a warning icon in the developer tools, which the lesson after next covers, and a validator catches them before they ship:
CSS Toolkit Its validator reports all four mistakes in broken.css with their line numbers, and suggests color for colr.A bad selector costs the whole rule
A mistake in a selector costs more than a mistake in a declaration. When the parser cannot make sense of a selector,
it drops the rule together with its whole declaration block, and a selector list fails as a unit. In
h2, h3:hovr { color: rebeccapurple; }, where :hovr is a misspelt :hover, no h2 turns purple either: all three
engines dropped the rule. The exception is the :is() pseudo-class, which takes a forgiving list. In
:is(h4, h5:hovr) the bad selector is skipped and h4 still matches, in all three engines. The selectors module of
this track covers :is() and its relatives.
Why the parser forgives: fallbacks
The forgiving parser is deliberate. New syntax looks like an error to an older parser, so an older browser drops what it does not understand and still uses everything else in the stylesheet. That gives you a simple way to write a fallback: put the declaration every browser understands first, and the newer one after it.
.hero {
min-height: 100vh; /* every browser: the viewport's height */
min-height: 100dvh; /* newer browsers: the viewport's height right now,
with or without a phone's address bar */
} A browser that knows the dvh unit accepts both declarations, and the second wins because it comes later. An older
browser drops the second as invalid and keeps the first. This works because of one more rule in the specifications:
when any part of a value is not supported, the whole declaration is dropped, never just the part it does not know.
Dynamic viewport units now work in every major browser, so this particular fallback only helps old ones. The last
lesson of this module shows how to decide when a fallback can go.
CSS has no version number
The name “CSS3” is still common, but CSS has no version 3. CSS 1 and CSS 2 were single documents. After CSS 2.1, the CSS Working Group split the language into modules, such as Selectors, Color and Grid Layout, and each module moves through its own levels. Selectors Level 4 builds on Selectors Level 3, and a module with nothing before it starts at Level 1. “CSS Level 3” only ever described that first generation of modules, and the Working Group’s own definition says outright that there is no CSS Level 4.
To describe CSS as a whole, the Working Group publishes a snapshot most years. CSS Snapshot 2026, a W3C Group Note, lists the specifications it treats as the official definition of CSS (CSS Level 2, Syntax Level 3, Selectors Level 3, Flexbox Level 1, Color Level 4 and others), then reliable Candidate Recommendations such as Grid Level 2 and Cascade Level 5, then modules that are less settled. The snapshot is written for the people who build browsers, and it sorts specifications by how stable they are, not by which browsers support them. Browser support is a separate question with its own source, Baseline, which the last lesson of this module explains.
Try it: fix the stylesheet
Paste broken.css into the validator above and fix the four mistakes until it reports nothing. If you also want to
see the result, put the CSS into the HTML, CSS & JS Playground with an <h1> and a <p class="note">. Then compare
your version with the corrected file, the one the program checked: all seven declarations kept.
h1 {
color: #1d3557;
font-size: 2.5rem;
margin-block: 0 0.5rem;
}
.note {
/* a reminder for the team */
color: #2a6f4e;
background: #eef7f1;
padding: 0.75rem 1rem;
border-inline-start: 4px solid #2a6f4e;
} Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Why doesn’t one invalid declaration break the rest of a CSS rule?
The CSS parser is built to recover from errors, as CSS Syntax Level 3 describes. Inside a declaration block, a piece it cannot read is thrown away up to the next semicolon, and a declaration whose value does not match its property’s grammar is dropped on its own; parsing then carries on, so the other declarations still apply. This is deliberate: new syntax looks like an error to an older parser, and a browser that dropped whole stylesheets on the first unknown property could never be used with newer CSS. The same rule makes fallbacks work: a widely supported declaration first, the newer one after it. The cost is silence. Nothing appears in the console, so a typo or a missing semicolon can remove styles unnoticed until someone checks DevTools or runs a validator.
Key takeaways
- A browser parses HTML into the DOM and CSS into the CSSOM, computes one value per property for every element through selector matching and the cascade, then runs layout, paint and composite.
- A rule is a selector and a declaration block; a declaration is a property, a colon and a value; at-rules start with
@; the only comment syntax is/* … */. - A shorthand sets every longhand it covers and resets the ones you leave out.
- The parser drops what it cannot read up to the next semicolon, and drops a declaration whose value is invalid; the rest of the rule still applies, and nothing is reported.
- A bad selector drops its whole rule, and one bad selector in a list drops the rule for all of them, except inside
:is(). - CSS has no versions: modules have levels, and the yearly snapshot sorts them by specification stability, not by browser support.
References
- CSS Snapshot 2026 (W3C CSS Working Group)
- CSS Syntax Module Level 3 (W3C CSS Working Group)
- Selectors Level 4 (invalid selectors and error handling) (W3C CSS Working Group)
- CSS Cascading and Inheritance Level 5 (W3C CSS Working Group)
- CSS Values and Units Module Level 4 (viewport-percentage lengths) (W3C CSS Working Group)
- Rendering performance (Google (web.dev))
- CSS: Cascading Style Sheets (MDN Web Docs) (Mozilla)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress