Skip to main content
The CSS engine transforms stylesheets into computed styles for each element. This involves tokenization, parsing CSS syntax, matching selectors to elements, resolving the cascade, and computing final style values.

CSS tokenizer and parser

CSS parsing begins with tokenization, converting the raw stylesheet text into tokens, followed by parsing those tokens according to CSS grammar rules.

Tokenization process

The CSS tokenizer breaks the input stream into meaningful tokens:
1

Read input stream

Process the stylesheet character by character, handling Unicode and escape sequences.
2

Recognize token patterns

Identify different token types based on character patterns:
  • Identifiers (property names, selector names)
  • Functions (e.g., rgb(), calc())
  • Strings (quoted values)
  • Numbers and dimensions (e.g., 10px, 1.5em)
  • Delimiters (:, ;, {, }, ,)
  • Operators (+, -, *, /)
  • Hash tokens (#id, #color)
  • At-keywords (@media, @keyframes)
3

Handle whitespace and comments

CSS comments (/* ... */) are discarded during tokenization. Whitespace is significant in some contexts (e.g., selector combinators) but not others.
4

Emit tokens

Send tokens to the parser for syntax analysis and structure building.
enum class CSSTokenType {
    Ident,
    Function,
    AtKeyword,
    Hash,
    String,
    Number,
    Dimension,
    Percentage,
    Delim,
    Whitespace,
    Colon,
    Semicolon,
    Comma,
    LeftBrace,
    RightBrace,
    LeftParen,
    RightParen,
    EOF
};

struct CSSToken {
    CSSTokenType type;
    std::string value;
    double numericValue;
    std::string unit;
};

class CSSTokenizer {
private:
    std::string input;
    size_t position;
    
public:
    CSSToken nextToken() {
        skipWhitespace();
        
        if (position >= input.length()) {
            return {CSSTokenType::EOF, "", 0, ""};
        }
        
        char current = input[position];
        
        // Identifier or function
        if (isIdentStart(current)) {
            std::string ident = consumeIdent();
            if (peek() == '(') {
                position++; // consume '('
                return {CSSTokenType::Function, ident, 0, ""};
            }
            return {CSSTokenType::Ident, ident, 0, ""};
        }
        
        // Number or dimension
        if (isDigit(current) || (current == '.' && isDigit(peek()))) {
            return consumeNumericToken();
        }
        
        // Hash (color or ID)
        if (current == '#') {
            position++;
            std::string value = consumeIdent();
            return {CSSTokenType::Hash, value, 0, ""};
        }
        
        // String
        if (current == '"' || current == '\'') {
            return consumeString(current);
        }
        
        // At-keyword
        if (current == '@') {
            position++;
            std::string keyword = consumeIdent();
            return {CSSTokenType::AtKeyword, keyword, 0, ""};
        }
        
        // Delimiters
        position++;
        switch (current) {
            case ':': return {CSSTokenType::Colon, ":", 0, ""};
            case ';': return {CSSTokenType::Semicolon, ";", 0, ""};
            case ',': return {CSSTokenType::Comma, ",", 0, ""};
            case '{': return {CSSTokenType::LeftBrace, "{", 0, ""};
            case '}': return {CSSTokenType::RightBrace, "}", 0, ""};
            case '(': return {CSSTokenType::LeftParen, "(", 0, ""};
            case ')': return {CSSTokenType::RightParen, ")", 0, ""};
            default:  return {CSSTokenType::Delim, std::string(1, current), 0, ""};
        }
    }
    
    CSSToken consumeNumericToken() {
        double value = consumeNumber();
        
        if (isIdentStart(peek())) {
            std::string unit = consumeIdent();
            if (unit == "%") {
                return {CSSTokenType::Percentage, "", value, "%"};
            }
            return {CSSTokenType::Dimension, "", value, unit};
        }
        
        return {CSSTokenType::Number, "", value, ""};
    }
};

Grammar implementation

The CSS parser constructs a structured representation (CSSOM - CSS Object Model) by following CSS grammar rules.
CSS uses a relatively simple grammar compared to programming languages, but it must handle error recovery gracefully since invalid CSS rules should be ignored rather than causing parsing to fail.
A stylesheet consists of a list of rules and at-rules:
stylesheet
  : [ rule | at-rule | whitespace ]*
  ;
At-rules include @import, @media, @keyframes, @font-face, etc.
Each rule consists of selectors and a declaration block:
rule
  : selectors '{' declarations '}'
  ;

selectors
  : selector [ ',' selector ]*
  ;
Example: h1, h2 { color: blue; font-size: 2em; }
Selectors are composed of simple selectors and combinators:
selector
  : simple-selector [ combinator simple-selector ]*
  ;

simple-selector
  : element-name? [ '#' id | '.' class | '[' attribute ']' | ':' pseudo ]*
  ;

combinator
  : ' ' | '>' | '+' | '~'
  ;
Examples:
  • div.container > p (descendant and child combinators)
  • a:hover::before (pseudo-class and pseudo-element)
  • input[type="text"] (attribute selector)
Declarations consist of property-value pairs:
declarations
  : declaration [ ';' declaration ]* [ ';' ]?
  ;

declaration
  : property ':' value [ '!' 'important' ]?
  ;
The value can be keywords, numbers, colors, functions, or combinations.
class CSSParser {
private:
    CSSTokenizer tokenizer;
    CSSToken currentToken;
    
public:
    Stylesheet parseStylesheet() {
        Stylesheet stylesheet;
        
        while (currentToken.type != CSSTokenType::EOF) {
            if (currentToken.type == CSSTokenType::AtKeyword) {
                stylesheet.rules.push_back(parseAtRule());
            }
            else {
                auto rule = parseRule();
                if (rule) {
                    stylesheet.rules.push_back(*rule);
                }
            }
        }
        
        return stylesheet;
    }
    
    std::optional<StyleRule> parseRule() {
        // Parse selectors
        auto selectors = parseSelectors();
        if (selectors.empty()) {
            return std::nullopt;
        }
        
        // Expect '{'
        if (currentToken.type != CSSTokenType::LeftBrace) {
            skipToNextRule();
            return std::nullopt;
        }
        advance();
        
        // Parse declarations
        auto declarations = parseDeclarations();
        
        // Expect '}'
        if (currentToken.type != CSSTokenType::RightBrace) {
            skipToNextRule();
            return std::nullopt;
        }
        advance();
        
        return StyleRule{selectors, declarations};
    }
    
    std::vector<Selector> parseSelectors() {
        std::vector<Selector> selectors;
        
        do {
            auto selector = parseSelector();
            if (selector) {
                selectors.push_back(*selector);
            }
            
            if (currentToken.type == CSSTokenType::Comma) {
                advance();
            }
            else {
                break;
            }
        } while (true);
        
        return selectors;
    }
    
    std::vector<Declaration> parseDeclarations() {
        std::vector<Declaration> declarations;
        
        while (currentToken.type != CSSTokenType::RightBrace && 
               currentToken.type != CSSTokenType::EOF) {
            
            if (currentToken.type == CSSTokenType::Ident) {
                auto decl = parseDeclaration();
                if (decl) {
                    declarations.push_back(*decl);
                }
            }
            
            if (currentToken.type == CSSTokenType::Semicolon) {
                advance();
            }
        }
        
        return declarations;
    }
    
    std::optional<Declaration> parseDeclaration() {
        std::string property = currentToken.value;
        advance();
        
        // Expect ':'
        if (currentToken.type != CSSTokenType::Colon) {
            return std::nullopt;
        }
        advance();
        
        // Parse value
        auto value = parseValue();
        if (!value) {
            return std::nullopt;
        }
        
        // Check for !important
        bool important = false;
        if (currentToken.type == CSSTokenType::Delim && currentToken.value == "!") {
            advance();
            if (currentToken.type == CSSTokenType::Ident && currentToken.value == "important") {
                important = true;
                advance();
            }
        }
        
        return Declaration{property, *value, important};
    }
};
CSS parsers must implement error recovery by skipping invalid rules and continuing to parse the rest of the stylesheet. A single syntax error should not prevent other valid rules from being applied.

Media query evaluation

Media queries allow conditional application of styles based on device characteristics.
Basic media types target different output devices:
@media screen {
  /* Styles for screens */
}

@media print {
  /* Styles for printing */
}

@media all {
  /* Applies to all media types */
}
Media queries are evaluated at stylesheet parse time and whenever the relevant device characteristics change (viewport resize, orientation change, etc.). The browser maintains an active set of matching media queries to determine which rules apply.
You now understand the CSS engine’s tokenization and parsing phases. Next, we’ll explore how parsed CSS rules are matched to HTML elements through selector matching.

Build docs developers (and LLMs) love