How Ladybird Applies CSS Styling and Layout to Web Content: Inside the LibWeb Engine

Ladybird applies CSS styling and layout through a four-phase pipeline implemented in LibWeb: CSS parsing creates style sheets, selector matching produces computed properties, the layout tree calculates geometry using formatting contexts, and painting renders the final output.

Ladybird renders web content using a classic "Parse → Style → Layout → Paint" model implemented in its LibWeb component. This architecture separates style computation from geometry calculation, enabling efficient incremental updates when the DOM or CSS changes. The CSS styling and layout engine relies on specific C++ classes in the Libraries/LibWeb/ directory to transform raw HTML and CSS into rendered pixels.

The Rendering Pipeline Architecture

Ladybird's rendering engine follows a strict pipeline where each phase feeds into the next. First, the CSS parser processes style sheets into structured objects. Next, the selector engine matches rules to DOM elements and resolves the cascade. Then, the layout system creates a parallel tree of layout nodes that calculate positions and sizes. Finally, the paint phase draws these boxes to the screen according to stacking context rules.

Phase 1: CSS Parsing and Style Sheet Construction

When a document loads, Ladybird parses <style> elements and external style sheets in LibWeb/CSS/Parser.cpp. The parser generates CSS::StyleSheet objects containing trees of rules such as CSS::CSSStyleRule and CSS::CSSKeyframesRule.

These style sheets remain in memory as structured objects until the engine needs to determine which rules apply to specific DOM elements.

Phase 2: Selector Matching and Cascade Resolution

For each DOM element, Ladybird runs the selector-matching algorithm defined in LibWeb/CSS/SelectorEngine.cpp. The engine evaluates selectors against the element's attributes, parentage, and DOM state.

Matching rules are ordered according to CSS cascade rules: importance, origin, specificity, and document order. The final resolved values are stored in a CSS::ComputedProperties object that lives directly on the element. This data structure, defined in Libraries/LibWeb/CSS/ComputedProperties.h, holds all resolved style values for properties like display, margin, and background-color.

Phase 3: Creating the Layout Tree

When the document requires layout, each DOM element creates a corresponding layout node via a virtual create_layout_node method. This method instantiates specific Layout::Node subclasses based on the element type and computed display properties.

For example, SVG elements in Libraries/LibWeb/SVG/SVGUseElement.cpp implement this pattern:

// Libraries/LibWeb/SVG/SVGUseElement.cpp
GC::Ptr<Layout::Node> SVGUseElement::create_layout_node(GC::Ref<CSS::ComputedProperties> style) {
    return heap().allocate<Layout::SVGGraphicsBox>(document(), *this, move(style));
}

Standard HTML elements follow the same pattern. An HTMLDivElement calls create_layout_node to return a Layout::BlockBox, while inline elements generate Layout::InlineBox instances. All layout node classes reside under Libraries/LibWeb/Layout/, including BlockBox.h, InlineBox.h, and specialized formatting context boxes.

Phase 4: Layout Calculation and Painting

Once the layout tree exists, each node executes its layout algorithm through Layout::Node::layout. This method reads computed style values—such as margin, padding, and display—through the CSS::ComputedProperties interface (e.g., computed_properties.display(), computed_properties.border_spacing_horizontal()).

The layout pass proceeds depth-first and includes:

After layout completes, each Layout::Node paints itself onto a bitmap through its paint() method, following the stacking context hierarchy to ensure correct z-ordering.

Handling Dynamic Style Changes

When JavaScript or the CSSOM mutates a style, Ladybird updates the element's CSS::ComputedProperties and marks the layout tree dirty. The next visual update triggers a re-layout of only the affected subtree.

The View Transitions API demonstrates this mechanism in Libraries/LibWeb/ViewTransition/ViewTransition.cpp:

// Libraries/LibWeb/ViewTransition/ViewTransition.cpp
ErrorOr<void> ViewTransition::update_pseudo_element_styles() {
    // … creates/updates CSSStyleRule objects in a dynamic stylesheet.
}

This function injects dynamic CSS rules that the layout engine consumes on the next layout pass, ensuring animations and transitions recalculate geometry correctly.

Working with Ladybird's Style and Layout APIs

You can interact with Ladybird's styling system programmatically. To retrieve computed styles from C++:

// Retrieve the computed style of an element (C++ API)
auto& element = document.get_element_by_id("my-div");
auto const& computed = element.computed_css_values();

// Query a specific property
CSS::Color bg = computed.property(Web::CSS::PropertyID::BackgroundColor).to_color();

To force document layout and painting after programmatic changes:

// Force a layout of the whole document (e.g. after a style change)
document.layout();          // builds/updates the Layout::Node tree
document.paint_all();       // triggers painting

From JavaScript, style changes automatically trigger the dirty-checking mechanism:

// Change a style from JavaScript and let Ladybird re‑layout automatically
let el = document.getElementById('my-div');
el.style.setProperty('margin-left', '20px');   // updates ComputedProperties
// Ladybird marks the layout node dirty; the next frame runs layout+paint.

Summary

  • CSS parsing happens in LibWeb/CSS/Parser.cpp, producing CSS::StyleSheet objects containing rule trees.
  • Selector matching runs in LibWeb/CSS/SelectorEngine.cpp, applying cascade rules to generate CSS::ComputedProperties stored on each element.
  • Layout tree construction uses virtual create_layout_node methods (as seen in SVGUseElement.cpp) to generate Layout::Node instances from DOM elements.
  • Layout calculation executes depth-first in Layout::Node::layout, using formatting contexts like FlexFormattingContext.cpp and GridFormattingContext.cpp to resolve geometry.
  • Dynamic updates modify ComputedProperties and mark layout nodes dirty, with specialized handling for view transitions in ViewTransition.cpp.

Frequently Asked Questions

How does Ladybird resolve CSS specificity and cascade conflicts?

Ladybird implements standard CSS cascade resolution in LibWeb/CSS/SelectorEngine.cpp. The engine sorts matching declarations by importance (whether !important is present), origin (author vs. user vs. user-agent), selector specificity, and finally source order. The winning declarations populate the CSS::ComputedProperties object attached to each DOM element.

What is the relationship between DOM elements and layout nodes?

Each visible DOM element creates exactly one layout node through its create_layout_node method. This layout node holds a reference to the element's ComputedProperties and implements layout algorithms specific to its display type (block, inline, flex, or grid). The separation allows the DOM to change independently while the layout tree handles geometry calculations.

How does Ladybird optimize layout recalculation when styles change?

Ladybird uses a dirty-bit system. When JavaScript or CSSOM updates modify an element's ComputedProperties, the engine marks that element's layout node and its ancestors as dirty. The next frame runs a partial re-layout starting from the dirty nodes rather than recalculating the entire document tree, as implemented in the layout scheduling logic within Libraries/LibWeb/Layout/.

Which modern layout algorithms does Ladybird support?

According to the source code in Libraries/LibWeb/Layout/, Ladybird supports Flexbox via FlexFormattingContext.cpp and CSS Grid via GridFormattingContext.cpp. These implement the full algorithms for distributing space and aligning items within flex and grid containers, reading properties like justify-content and grid-template-columns from the computed style objects.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →