Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 20 minutes
  • Examples run with Node.js 24.21.0
  • By MySmartCoPilot
Free: 20 minutes

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.

The rendering pipeline: HTML and CSS are parsed into the DOM and the CSSOM, then style, layout, paint and composite produce the page.HTML bytesCSS bytesDOMthe tree of elementsCSSOMstyle sheets and their rulesStylematch selectors, run the cascade,compute one value per propertyLayoutevery box gets a size and a positionPaintboxes are drawn as pixels, in layersCompositethe layers are combined on screenparseparse, dropping what it cannot read

From bytes to pixels

Text description of the diagram

The diagram shows the stages a browser goes through, from top to bottom.

  1. The HTML bytes are parsed into the DOM, the tree of elements.
  2. The CSS bytes are parsed into the CSSOM, the style sheets and their rules. The parser drops whatever it cannot read.
  3. Style combines the two: the browser matches selectors against elements, runs the cascade and computes one value for every property of every element.
  4. Layout gives every box a size and a position.
  5. Paint draws the boxes as pixels, in layers.
  6. 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:

  1. Parse. The HTML parser builds the DOM. Every piece of CSS, whether it comes from a file, a <style> element or a style attribute, 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.
  2. 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 a style attribute, 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.
  3. Layout. Elements become boxes, and each box gets a size and a position. The display and position values computed in the previous step decide which layout rules apply: normal flow, flexbox, grid or positioning.
  4. 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 small stylesheet with every part of a rule CSS · first-rules.css
/* 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.

A stylesheet with four mistakes CSS · broken.css
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 pieces of the two stylesheets survive JavaScript · recovery.mjs
// 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, only margin-block was kept. The heading stayed in the default black, at the default 32px.
  • .note: of the 4 declarations written, only border-inline-start was kept. The note had black text, no background and no padding.

Here is what went wrong in each case:

  • colr is 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 rem has a space between the number and its unit. A length such as 2.5rem is written as one unit of syntax; with the space, the value is a number followed by a word, which font-size does 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 includes color: #2a6f4e on 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: #eef7f1 joins 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.

A fallback, then the newer value CSS · fallback.css
.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.

The corrected stylesheet CSS · fixed.css
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.

  1. Question 1 of 6 In h1, h2 { color: #1d3557; }, which part is the declaration?

    Choose one answer.

    Show the answer to question 1

    Answer: color: #1d3557;

    A declaration is a property, a colon and a value, here color: #1d3557, ended by a semicolon. h1, h2 is the selector list, #1d3557 is the value alone, and the braces with everything inside them form the declaration block.

  2. Question 2 of 6 Which declarations of this rule does a browser apply?

    Read the code, then choose one answer.

    .price {
      font-size: 1.25rem;
      // sale price
      color: #b00020;
      font-weight: 700;
    }
    Show the answer to question 2

    Answer: font-size and font-weight, but not color

    // is not a comment in CSS. The parser cannot read // sale price color: #b00020 as a declaration, so it throws it away up to the next semicolon, which ends the color line. Parsing then starts again, so font-weight: 700 still applies, and so does font-size before it.

  3. Question 3 of 6 Which declarations of this rule does a browser apply?

    Read the code, then choose one answer.

    .tag {
      padding: 0.25rem 0.5rem
      border-radius: 4px;
      color: #1d3557;
    }
    Show the answer to question 3

    Answer: Only color

    Without the semicolon after 0.5rem, the parser reads one declaration, padding, whose value is 0.25rem 0.5rem border-radius: 4px. That is not a valid padding, so the whole declaration, and with it the border radius, is dropped. color comes after the next semicolon and applies normally.

  4. Question 4 of 6 A stylesheet contains .btn, .btn:hoverr { cursor: pointer; }, with :hover misspelt. What happens to elements with the class btn?

    Choose one answer.

    Show the answer to question 4

    Answer: They do not get cursor: pointer: the whole rule is dropped

    A selector list containing an invalid selector is invalid as a whole, so the rule and its block are dropped and .btn is not styled either. Writing :is(.btn, .btn:hoverr) would keep .btn working, because :is() takes a forgiving list, but the real fix is the spelling.

  5. Question 5 of 6 A rule says min-height: 100vh; and then min-height: 100dvh;. A browser that does not know the dvh unit reads it. Which value does it use?

    Choose one answer.

    Show the answer to question 5

    Answer: 100vh, because it drops the second declaration as invalid

    The later declaration wins only if it is valid. An unknown unit makes the value invalid, so this browser drops the second declaration and the first one, which it understands, applies. A newer browser accepts both, and the second wins.

  6. Question 6 of 6 Which of these statements about CSS versions are true?

    Choose every answer that is right.

    Show the answer to question 6

    Answer:

    • There is no CSS Level 4 of the language as a whole
    • Each CSS module has its own levels, such as Selectors Level 4

    Since CSS 2.1, CSS is split into modules that each move through levels, and the CSS Working Group states that there is no CSS Level 4. "CSS3" is only a historical label. The snapshot sorts specifications by how stable they are, for browser makers; browser support is what Baseline reports.

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.