The layout system takes the DOM tree with computed CSS properties and produces a tree of positioned, sized boxes that can be painted. This page covers the layout tree construction via TreeBuilder, the FormattingContext class hierarchy that computes positions and sizes, the LayoutState and UsedValues data structures that accumulate layout results during a pass, and how LayoutState::commit() transfers those results into the Paintable tree.
For how computed CSS property values are derived from style rules, see CSS Style Computation and Animations. For how the resulting paintable tree is rendered, see Painting and Display Lists.
The DOM tree is not used directly for layout. TreeBuilder (Libraries/LibWeb/Layout/TreeBuilder.cpp) walks the DOM and constructs a parallel layout tree of Layout::Node objects. Each DOM element typically produces one layout node, but the relationship is not always 1:1.
display: none produce no layout node.display: contents produce no box of their own; their children are promoted into the parent layout context.::before, ::after, ::marker, ::backdrop) produce layout nodes synthesized by TreeBuilder::create_pseudo_element_if_needed(), based on the element's computed ContentData and display property.Two helper functions enforce the invariant when inserting nodes:
| Function | Behavior |
|---|---|
insertion_parent_for_inline_node() | If the parent already has in-flow block children, finds or creates an anonymous inline wrapper as the insertion point. |
insertion_parent_for_block_node() | If the parent has existing inline children, wraps them in a new anonymous block wrapper before inserting the block node. |
When a block-level element appears inside an inline ancestor (e.g., a <div> inside a <span>), TreeBuilder::restructure_block_node_in_inline_parent() performs the CSS block-in-inline split. It creates "before", "middle", and "after" anonymous wrappers, and records a continuation chain via NodeWithStyleAndBoxModelMetrics::continuation_of_node() so that the inline paintable can be reconstructed correctly across the split.
Sources: Libraries/LibWeb/Layout/TreeBuilder.cpp44-192 Libraries/LibWeb/Layout/TreeBuilder.cpp393-605
Figure: DOM to Layout Tree Construction
Sources: Libraries/LibWeb/Layout/TreeBuilder.cpp68-192 Libraries/LibWeb/Layout/TreeBuilder.cpp429-600
All layout nodes live in the Web::Layout namespace, are GC-managed (JS::Cell), and form a tree via TreeNode<Node>.
Figure: Layout Node Class Hierarchy (code entity mapping)
Sources: Libraries/LibWeb/Layout/Node.h35-350
| Class | Role |
|---|---|
Node | Base for all layout nodes; holds DOM reference and associated paintable list |
NodeWithStyle | Adds computed CSS values (ImmutableComputedValues) and a layout_index for LayoutState lookup |
NodeWithStyleAndBoxModelMetrics | Adds the continuation chain pointer for block-in-inline splits |
Box | A box with geometry; base for all dimensioned layout nodes |
BlockContainer | Block-level container for either inline or block children |
Viewport | Root of the layout tree; corresponds to the initial containing block |
InlineNode | Inline-level element (e.g. <span>); can produce multiple paintables (one per line) |
TextNode | Text content; produces LineBoxFragment records, not a box |
ReplacedBox | Replaced elements (e.g. <img>) with intrinsic dimensions |
TableWrapper | Anonymous block wrapper that absorbs a table box's margins |
SVGSVGBox | Root of an SVG subtree; triggers SVGFormattingContext |
Before a formatting context runs, it receives an AvailableSpace describing the space available in each axis. This is the primary mechanism for constraint propagation from parent to child.
AvailableSpace is a pair of AvailableSize values (one per axis). Each AvailableSize is one of:
| Variant | Meaning |
|---|---|
Definite(px) | A known pixel measurement is available |
Indefinite | Available space is unknown (e.g. auto-height parent) |
MinContent | Intrinsic sizing: compute the minimum content size |
MaxContent | Intrinsic sizing: compute the maximum content size |
The LayoutMode enum on every context run controls whether intrinsic sizing constraints are active:
LayoutMode | Meaning |
|---|---|
Normal | Regular layout pass; boxes use their containing block's actual size |
IntrinsicSizing | Computing min/max-content sizes; boxes treat their containing block as 0-width or ∞-width depending on constraint |
UsedValues::available_inner_space_or_constraints_from(outer_space) derives a child's AvailableSpace from the parent's, propagating intrinsic sizing constraints into axes where the child's own size is not yet definite. has_definite_width() returns true only when m_has_definite_width is set and width_constraint == SizeConstraint::None, preventing intrinsic-sizing passes from treating constrained sizes as settled.
Sources: Libraries/LibWeb/Layout/Node.h24-33 Libraries/LibWeb/Layout/LayoutState.h19-145
LayoutState is the write-only scratch space for a layout pass. It is created fresh for each full layout and discarded after commit(). Subtree re-layouts create a scoped LayoutState with a m_subtree_root pointer that restricts which nodes may be written.
Used values are kept in a PagedStore<UsedValues> (Libraries/LibWeb/Layout/LayoutState.h57-120) indexed by NodeWithStyle::layout_index(). This is a two-level page-table structure (16 entries per page) giving O(1) access without hashing, while only allocating pages for nodes that are actually touched.
LayoutState
└── m_used_values_store : PagedStore<UsedValues>
└── indexed by NodeWithStyle::layout_index()
get(index) → UsedValues* (read)
allocate(index) → UsedValues& (write)
LayoutState::get_mutable(node) and LayoutState::get(node) are the access points used throughout all formatting contexts.
| Field | Type | Description |
|---|---|---|
offset | CSSPixelPoint | Content-edge position relative to containing block's content edge |
m_content_width / height | CSSPixels | Content box dimensions |
margin_{left,right,top,bottom} | CSSPixels | Used margin values |
border_{left,right,top,bottom} | CSSPixels | Border widths |
padding_{left,right,top,bottom} | CSSPixels | Padding values |
inset_{left,right,top,bottom} | CSSPixels | Position insets (for position: relative/absolute) |
line_boxes | Vector<LineBox> | Line boxes for inline content |
width_constraint / height_constraint | SizeConstraint | Active intrinsic sizing constraint |
m_has_definite_width / height | bool | Whether the dimension is definitively resolved |
containing_line_box_fragment | Optional<LineBoxFragmentCoordinate> | For atomic inlines, which line box fragment holds this box |
Helper methods like margin_box_width(), border_box_height() etc. compose these fields for common use.
Sources: Libraries/LibWeb/Layout/LayoutState.h122-270 Libraries/LibWeb/Layout/LayoutState.cpp22-90
FormattingContext (Libraries/LibWeb/Layout/FormattingContext.h58-200) is the abstract base class for all layout algorithms. Each instance is responsible for sizing and positioning the boxes inside its context box.
Figure: FormattingContext subclass hierarchy (code entity mapping)
Sources: Libraries/LibWeb/Layout/FormattingContext.h58-200 Libraries/LibWeb/Layout/FormattingContext.cpp140-282
formatting_context_type_created_by_box() inspects a box's display value and other properties to determine which formatting context type it establishes:
| Box condition | Resulting Type |
|---|---|
display: flex | Flex |
display: grid | Grid |
display: table | Table |
<svg> root (SVGSVGBox) | SVG |
| Replaced element with children | ReplacedWithChildren |
| Replaced element | InternalReplaced |
| BFC-establishing conditions | Block |
| Inline children, no BFC | None (handled by parent BFC) |
creates_block_formatting_context() checks the full set of BFC-establishing conditions from the CSS spec: root element, floats, absolutely positioned, inline-block, table cells/captions, overflow ≠ visible/clip, display: flow-root, CSS containment (layout or paint), flex/grid items, multicol containers, and <fieldset>.
create_independent_formatting_context_if_needed() is the factory: it calls formatting_context_type_created_by_box() and constructs the appropriate subclass. layout_inside() wraps this factory and additionally skips layout entirely if the child already has definite sizes in both axes during intrinsic sizing.
Sources: Libraries/LibWeb/Layout/FormattingContext.cpp39-282
BlockFormattingContext (Libraries/LibWeb/Layout/BlockFormattingContext.cpp) is the most commonly invoked context. It handles:
layout_block_level_children()layout_inline_children(), which delegates to a nested InlineFormattingContextm_left_floats / m_right_floats; intrusion_by_floats_into_box() queries float encroachment at a given Yparent_context_did_dimension_child_root_box() firesm_margin_state and margins_collapse_through()compute_width() implements CSS 2.2 §10.3 for non-replaced block elements in three steps:
min-width / max-width.max-width, recompute with max-width as input.min-width, recompute with min-width as input.For floating boxes, compute_width_for_floating_box() uses shrink-to-fit:
min(max(preferred_minimum_width, available_width), preferred_width)
Auto margins are resolved in try_compute_width() by distributing the underflow between the two auto margins.
resolve_used_height_if_not_treated_as_auto() resolves explicit heights with min-height / max-height clamping. resolve_used_height_if_treated_as_auto() delegates to automatic_content_height() on the child formatting context, or to compute_auto_height_for_block_level_element(), which follows CSS 2.2 §10.6.3: the height is the distance to the bottom edge of the last line box (for inline children) or the bottom margin edge of the last in-flow block child.
compute_auto_height_for_block_formatting_context_root() (in FormattingContext) additionally extends the height to include any floating descendants whose bottom margin edge falls below the last in-flow child.
avoid_float_intrusions() implements the CSS rule that elements establishing a new formatting context must not overlap float margin boxes. It moves a box downward (one float height at a time) until the remaining horizontal space accommodates the box's border-box width.
Sources: Libraries/LibWeb/Layout/BlockFormattingContext.cpp34-750 Libraries/LibWeb/Layout/FormattingContext.cpp375-432
InlineFormattingContext (Libraries/LibWeb/Layout/InlineFormattingContext.cpp) lays out inline content into a sequence of line boxes. It is always created as a child of a BlockFormattingContext.
run() calls generate_line_boxes(), which uses InlineLevelIterator to yield inline-level items and LineBuilder to place them onto lines.
LineBuilder (Libraries/LibWeb/Layout/LineBuilder.cpp) manages current line state:
| Method | Action |
|---|---|
append_box() | Places an atomic inline (inline-block, replaced element) onto the current line |
append_text_chunk() | Places a text fragment with glyph run data |
break_line() | Finalises the current line; starts a new one, scanning past float intrusions |
break_if_needed() | Breaks only if the next item would overflow m_available_width_for_current_line |
y_for_float_to_be_inserted_here() | Computes the Y at which a float can be placed given current line state |
available_space_for_line(y) subtracts left and right float intrusions from the container width to get the actual usable inline width at a given block offset.
Line boxes are stored in UsedValues::line_boxes as a Vector<LineBox>. Each LineBox holds LineBoxFragment records describing position, size, font metrics, and source range (offset + length into a TextNode).
Sources: Libraries/LibWeb/Layout/InlineFormattingContext.cpp19-100 Libraries/LibWeb/Layout/LineBuilder.cpp1-180
FlexFormattingContext (Libraries/LibWeb/Layout/FlexFormattingContext.cpp) implements the CSS Flexbox Level 1 §9 layout algorithm. The run() method follows the spec's numbered steps:
| Step | Implementation |
|---|---|
| 1. Generate anonymous flex items | generate_anonymous_flex_items() |
| 2. Available main/cross space | determine_available_space_for_items() |
| 3. Flex base sizes | determine_flex_base_size() per item |
| 3b. Hypothetical main sizes | Clamped via automatic_minimum_size(), specified_main_min/max_size() |
| 5. Collect into lines | collect_flex_items_into_flex_lines() |
| 6. Resolve flexible lengths | resolve_flexible_lengths() |
| 7–11. Cross axis | determine_hypothetical_cross_size_of_item(), calculate_cross_size_of_each_flex_line(), determine_used_cross_size_of_each_flex_item() |
| 12–13. Distribute free space / auto margins | distribute_any_remaining_free_space(), resolve_cross_axis_auto_margins() |
| 14. Align items | align_all_flex_items_along_the_cross_axis() |
| 16. Align lines | align_all_flex_lines() |
| Final. Layout insides | layout_inside() per item |
Main-axis and cross-axis logic is abstracted through helpers (is_row_layout(), main_size(), cross_size(), inner_main_size(), etc.) so that row and column flex directions share one algorithm body.
Two optimisations skip the automatic_minimum_size calculation when it can be proven to have no effect, avoiding expensive intrinsic sizing passes.
Sources: Libraries/LibWeb/Layout/FlexFormattingContext.cpp56-242 Libraries/LibWeb/Layout/FlexFormattingContext.h14-200
GridFormattingContext (Libraries/LibWeb/Layout/GridFormattingContext.cpp) implements CSS Grid Layout Level 2.
Each track is a GridTrack struct:
GridTrack {
min_track_sizing_function : CSS::GridSize
max_track_sizing_function : CSS::GridSize
base_size : CSSPixels
growth_limit : Optional<CSSPixels>
is_gap : bool
is_auto_fit : bool
}
CSS::GridSize (Libraries/LibWeb/CSS/GridTrackSize.h20-51) wraps either a CSS::Size or a CSS::Flex (fr unit) and provides classification queries (is_definite(), is_fixed(), is_flexible_length(), is_intrinsic()).
Tracks are stored in m_grid_columns / m_grid_rows. Gap tracks (GridTrack::create_gap()) are interleaved into m_grid_columns_and_gaps / m_grid_rows_and_gaps so the track sizing algorithm can iterate them uniformly.
initialize_grid_tracks_from_definition() expands repeat() expressions (including auto-fill / auto-fit via count_of_repeated_auto_fill_or_fit_tracks()) into flat track vectors.
OccupationGrid tracks occupied cells in a HashTable<GridPosition>. The auto-placement algorithm runs in four passes:
place_item_with_row_and_column_position() — both axes explicitplace_item_with_row_position() — only row explicit; scans for free columnplace_item_with_column_position() — only column explicit; advances row cursorplace_item_with_no_declared_position() — fully auto-placed; uses OccupationGrid::find_unoccupied_place()resolve_grid_position() converts grid-row-start / grid-column-start CSS values (line numbers, named lines via get_nth_line_index_by_line_name(), spans) into numeric track indices.
initialize_track_sizes() sets each track's initial base_size and growth_limit from its sizing function. Subsequent steps — resolving intrinsic sizes, maximising tracks, expanding flexible fr tracks, and snapping — follow the CSS Grid §12 algorithm.
Sources: Libraries/LibWeb/Layout/GridFormattingContext.cpp151-630 Libraries/LibWeb/Layout/GridFormattingContext.h124-350 Libraries/LibWeb/CSS/GridTrackSize.h20-200
TableFormattingContext (Libraries/LibWeb/Layout/TableFormattingContext.cpp) handles display: table boxes. Table boxes are always wrapped in an anonymous TableWrapper (display: block) which absorbs the table's margins and participates in the parent BFC.
Key details:
run_caption_layout() runs block layout on display: table-caption elements above and below the table body.run_until_width_calculation() is a partial run used by FormattingContext::compute_table_box_width_inside_table_wrapper(): it creates a throwaway LayoutState and runs only through the column width computation (CSS 2.2 §17.5.2) without committing results, so the BFC can determine how wide the table wrapper should be.Sources: Libraries/LibWeb/Layout/TableFormattingContext.cpp17-110 Libraries/LibWeb/Layout/FormattingContext.cpp434-520
All layout computations use CSSPixels, a fixed-point numeric type. CSS::Length (Libraries/LibWeb/CSS/Length.h) represents a CSS <length> in any unit and converts to CSSPixels via:
| Unit category | Example units | Conversion method |
|---|---|---|
| Absolute | px, cm, mm, pt, in, Q, pc | absolute_length_to_px() — fixed multiplication factor |
| Font-relative | em, rem, ex, ch, cap, lh, rlh | font_relative_length_to_px(font_metrics, root_font_metrics) |
| Viewport-relative | vw, vh, vmin, vmax, svw, dvh, … | viewport_relative_length_to_px(viewport_rect) |
Length::ResolutionContext bundles the three inputs needed for resolution:
ResolutionContext {
viewport_rect : CSSPixelRect // current viewport size
font_metrics : FontMetrics // element's font (font_size, x_height, ...)
root_font_metrics : FontMetrics // root element's font (for rem, rex, ...)
}
Factory methods construct a ResolutionContext from available layout context:
| Factory | Used when |
|---|---|
ResolutionContext::for_layout_node(node) | Inside a formatting context with a live layout node |
ResolutionContext::for_element(element) | Style computation with a full AbstractElement |
ResolutionContext::for_document(document) | Document-level resolution (e.g. media queries) |
Percentage values are resolved by FormattingContext helpers such as calculate_inner_width() and calculate_inner_height(), which multiply the percentage fraction by the containing block's resolved dimension from UsedValues.
Sources: Libraries/LibWeb/CSS/Length.h22-120 Libraries/LibWeb/CSS/Length.cpp25-200
After all formatting contexts have run, LayoutState::commit(Box& root) transfers computed geometry into the Painting::Paintable tree. This is the boundary between the mutable layout pass and the stable paint tree.
Figure: LayoutState::commit() pipeline
Key points:
PaintableBox objects are cached by layout node pointer and reused via reset_for_relayout() to reduce GC pressure during partial re-layouts.InlineNode exclusion from cache: a single InlineNode can produce multiple PaintableWithLines objects — one per line — via create_paintable_for_line_with_index(). This 1:N relationship makes simple 1:1 caching inapplicable.try_to_relocate_fragment_in_inline_node() walks each fragment's parent chain to find an enclosing InlineNode and places the fragment in the correct per-line paintable, rather than in the block container's PaintableWithLines.resolve_relative_positions(): after the paint tree is connected, applies position: relative insets to inline fragments by accumulating offsets up the inline ancestor chain.Sources: Libraries/LibWeb/Layout/LayoutState.cpp240-500
Figure: Full layout pass sequence
Sources: Libraries/LibWeb/Layout/LayoutState.cpp256-500 Libraries/LibWeb/Layout/BlockFormattingContext.cpp34-155 Libraries/LibWeb/Layout/FormattingContext.cpp211-282
Refresh this wiki