Scientyfic World

CSS Parsing Explained: How Browsers Read CSS (Tokens, CSSOM and Error Recovery)

CSS parsing is how a browser turns the text of a stylesheet into a structure it can use. The parser first splits the CSS into tokens (identifiers, numbers with units,...

Share:

Get an AI summary of this article

CSS parsing featured image

CSS parsing is how a browser turns the text of a stylesheet into a structure it can use. The parser first splits the CSS into tokens (identifiers, numbers with units, colons, braces), then groups those tokens into rules and declarations, and the result becomes the CSS Object Model (CSSOM), which the browser matches against the DOM to style the page. CSS parsing is deliberately forgiving: an invalid declaration or rule is dropped and parsing continues, so one typo never breaks the whole stylesheet.

This guide walks through each stage with real output: the tokens produced by a spec-compliant tokenizer, the tree a CSS parser builds, and how a browser actually recovers from seven kinds of broken CSS, which I tested in a Chromium-based browser. If you’d rather build a parser yourself, see my step-by-step guide to making a CSS parser in JavaScript.

Where CSS Parsing Fits in Rendering

When a page loads, the browser parses HTML into the DOM and CSS into the CSSOM at the same time. It then computes the style of every element, runs layout and paints the page. Stylesheets in the <head> are render-blocking: the browser waits for them before the first paint, so the CSSOM is ready. The full pipeline is explained in how the browser rendering engine works, and the HTML side in what HTML parsing means.

The rules are defined in the CSS Syntax Module Level 3 specification, which all modern browsers implement, so the behaviour below should match across Chrome, Firefox and Safari (I tested it in a Chromium-based browser).

Step 1: Tokenization

The tokenizer reads the stylesheet character by character and produces a flat list of tokens. Comments are removed at this stage, and whitespace becomes whitespace tokens. Here is the output of @csstools/css-tokenizer, which follows the specification, for one rule:

import { tokenize } from '@csstools/css-tokenizer';

const css = `.example-class { color: blue; font-size: 16px; }`;
for (const [type, raw] of tokenize({ css })) {
  if (type === 'whitespace-token') continue;   // hide whitespace to keep the output short
  console.log(type.padEnd(16), JSON.stringify(raw));
}
delim-token      "."
ident-token      "example-class"
{-token          "{"
ident-token      "color"
colon-token      ":"
ident-token      "blue"
semicolon-token  ";"
ident-token      "font-size"
colon-token      ":"
dimension-token  "16px"
semicolon-token  ";"
}-token          "}"
EOF-token        ""

Notice that the tokenizer doesn’t know what a selector or a property is. .example-class is just a delim-token (the dot) followed by an ident-token, and color and blue are both identifiers. 16px is a single dimension-token holding the number 16 and the unit px. Other token types include hash-token (#title, #fff), string-token, url-token, at-keyword-token (@media), function-token (rgb(), percentage-token and number-token.

One thing the tokenizer settles: CSS has only one comment syntax, /* ... */. A line starting with // is not a comment, as the test below shows.

Step 2: Parsing into Rules and Declarations

The parser groups tokens into a tree: at-rules such as @media and @import, style rules made of a selector (the “prelude”) and a block, and declarations of a property and a value. Selectors and values are parsed further by grammars specific to each feature. With postcss, a popular JavaScript CSS parser, you can see the tree, including native CSS nesting:

import postcss from 'postcss';

const root = postcss.parse(`.card { color: blue; & .title { font-weight: bold } }`);
root.walk((node) => {
  const depth = (n) => (n.parent && n.parent.type !== 'root' ? 1 + depth(n.parent) : 0);
  const label = node.type === 'rule' ? `rule  selector="${node.selector}"` : `decl  ${node.prop}: ${node.value}`;
  console.log('  '.repeat(depth(node)) + label);
});
rule  selector=".card"
  decl  color: blue
  rule  selector="& .title"
    decl  font-weight: bold

Tools such as PostCSS, Lightning CSS, Sass, Stylelint and minifiers all start with this kind of tree.

Step 3: Building the CSSOM

In the browser, the parsed rules become the CSS Object Model: document.styleSheets, each with a cssRules list of CSSStyleRule, CSSMediaRule and other objects that JavaScript can read and change. The CSSOM only contains what parsed successfully, which makes it a handy way to see exactly what the browser kept.

const rules = [...document.styleSheets[0].cssRules].map((r) => r.cssText);
CSS.supports('display', 'grid');              // true
CSS.supports('display', 'grid-ish');          // false
CSS.supports('selector(:has(a))');            // true
CSS.supports('color', 'oklch(70% 0.1 250)');  // true

Those four CSS.supports() results are from my test browser; the same checks are available in CSS with @supports.

How Browsers Recover from Broken CSS (Tested)

Unlike most programming languages, CSS never fails as a whole. The specification defines exactly how to skip a bad piece and resume. I loaded this stylesheet with seven deliberate mistakes:

/* 1. A misspelled property */
.a { color: red; colr: blue; width: 10px; }

/* 2. A missing semicolon */
.b { color: red width: 10px; margin: 4px; }

/* 3. An invalid value */
.c { width: 10pixels; height: 20px; }

/* 4. One bad selector in a list */
.d, .e:hover-ish { color: red; }

/* 5. "//" is not a CSS comment */
// highlight the title
.f { color: green; }
.g { color: blue; }

/* 6. Native nesting */
.card {
  padding: 8px;
  & .title { font-weight: bold; }
}

/* 7. An unclosed block at the end of the file */
.h { color: purple;

And read back the CSSOM and computed styles:

.a { color: red; width: 10px; }
.b { margin: 4px; }
.c { height: 20px; }
.g { color: blue; }
.card { padding: 8px; & .title { font-weight: bold; } }
.h { color: purple; }
MistakeWhat the parser did
1. colr: blueDropped only that declaration; color and width kept
2. Missing semicolon after redcolor: red width: 10px became one invalid declaration and both were lost; margin kept
3. width: 10pixelsDropped the invalid value; height kept
4. .d, .e:hover-ishDropped the whole rule: one invalid selector in a list invalidates the list, so .d got no color
5. // highlight the titleNot a comment: the text became part of the next selector, so the .f rule was silently lost (.f stayed black) while .g worked
6. Native nestingParsed correctly; .title inside .card was bold
7. Unclosed block at the endClosed automatically at the end of the file; .h was purple

Two practical lessons: a missing semicolon can take out the declaration after it too, and an invalid selector in a comma-separated list kills the entire rule. If you need a list to tolerate unknown selectors, wrap it in :is() or :where(), which use forgiving parsing. In my test, :is(.x, .y:hover-ish) was kept as :is(.x) and still styled .x.

Step 4: The Cascade Decides Which Value Wins

Parsing only builds the list of rules. When several rules set the same property on an element, the cascade decides, in this order: origin and !important, cascade layers, specificity, then source order (the last one wins).

#title       { color: red; }    /* specificity (1, 0, 0) */
.page .title { color: green; }  /* (0, 2, 0) */
h1.title     { color: blue; }   /* (0, 1, 1) */
h1           { color: gray !important; }

On <h1 id="title" class="title"> inside .page, the browser chose red (the ID selector has the highest specificity). Adding the !important rule made it gray, even though h1 alone has the lowest specificity. Specificity counts IDs, then classes, attributes and pseudo-classes, then element names; :where() adds zero, and :is() and :not() take the specificity of their most specific argument.

Debugging CSS That Doesn’t Apply

  • In Chrome DevTools, the Styles pane shows invalid declarations with a warning icon and a strikethrough, and crossed-out declarations that were overridden by the cascade.
  • Read document.styleSheets[n].cssRules in the Console to see exactly which rules survived parsing.
  • Run a linter such as Stylelint, which catches unknown properties, invalid values and // comments before they ship.
  • If a whole rule is missing, check its selector list for one unsupported selector.

Conclusion

CSS parsing turns stylesheet text into tokens, then into rules and declarations, and finally into the CSSOM the browser uses to style the DOM. The parser is designed to keep going: in my tests it dropped a misspelled property, an invalid value and a bad rule while keeping everything around them. Knowing its rules explains some baffling bugs, like a // line silently deleting the rule below it, or one unsupported selector disabling a whole list.

FAQs

What is CSS parsing?

The process of reading a stylesheet, splitting it into tokens, grouping them into rules and declarations, and building the CSS Object Model (CSSOM) that the browser uses to style elements.

What happens when CSS has an error?

The browser drops the invalid part and continues. An invalid declaration is skipped, an invalid selector drops its whole rule, and an unclosed block is closed at the end of the file. The rest of the stylesheet still applies.

Is // a comment in CSS?

No. CSS only supports /* … */ comments. A line starting with // becomes part of the next selector, which usually makes that rule invalid so it is silently dropped. Sass and Less support // comments, but plain CSS does not.

What is the CSSOM?

The CSS Object Model: the parsed representation of all stylesheets, available in JavaScript as document.styleSheets. Together with the DOM, it is used to compute the style of every element.

Does CSS block page rendering?

Stylesheets in the head are render-blocking: the browser parses HTML while they download but waits for them before the first paint, so users do not see unstyled content.

What is the difference between a CSS parser and CSS parsing?

CSS parsing is the process; a CSS parser is the program that does it, such as the one inside every browser or libraries like PostCSS and Lightning CSS.

Snehasish Konger
Developed @scientyficworld.org | Technical writer @Nected | Content Developer
Connect with Snehasish Konger

On This page

Take a Pause with Intervals

A Sunday letter on building, writing, and thinking deeper as a developer — short, honest, and worth your time.

Snehasish Konger profile photo

"Hey there — I'm Snehasish. Hope this post saved you some head-scratching time! I've spent years turning technical chaos into clarity, and I'm here to be your guide through the maze of modern tech. Stick around for more lightbulb moments — we're just getting started."

Related Posts