Styling Subsystem

Overview

Azul's styling subsystem turns CSS source — author stylesheets, inline style="..." strings, runtime with_css(...) overrides, the user-agent default sheet, and OS-derived system colours — into per-node resolved values that the layout solver and renderer can read in O(1). The pipeline runs end to end in three stages: a hand-written parser produces typed CssProperty values from &str, a cascade pass resolves those values per node into a CssPropertyCache, and a final encoder packs the layout-hot subset into a four-array CompactLayoutCache keyed by node index.

Each stage has its own dedicated page. The parser is documented at CSS Parser. The cascade — selector matching, specificity, inheritance, the four-level RelayoutScope classification — is at Cascade, Inheritance, Restyle. The fast-path encoding for the ~50 layout-critical properties is at Compact Property Cache. OS theme discovery, which feeds the user-agent default colour, font, and metric values into every cascade, is at System Style Discovery.

This page is the orientation. It describes how the four pieces interact, what flows through each stage, and what to read first depending on the task.

The five priority sources

Every CSS property the renderer asks for is the resolution of five priority sources, lowest to highest:

  1. User-agent CSS. Per-NodeType defaults (<h1> is font-size: 2em font-weight: bold, <button> is display: inline-block with padding and a border). Hard-coded into apply_ua_css_to_compact so the common case bypasses the cascade walk entirely.
  2. Author * rules. Stylesheet rules with the universal selector. Hoisted into a single global_css_props: Vec<CssProperty> so they aren't cloned into every node's per-node prop list.
  3. Author specific selectors. Stylesheet rules whose selector matched the node. Stored in cascaded_props (parents) and css_props (per-node).
  4. Inline style="..." and NodeData::style. Each node carries an inline Css whose rules are tagged rule_priority::INLINE; the cascade walks them via Css::iter_inline_properties().
  5. Runtime callback overrides. Properties set by callbacks via CallbackInfo::set_css_property(...). Stored in user_overridden_properties. The highest priority, intended for stateful UI (focus rings, drag highlights, animation overrides).

The cascade reads in priority order top to bottom, short-circuiting at the first match. Per-node, the resolution is one O(1) array lookup if the property is in the compact cache, or a walk through the five sources for slow-path properties.

Stage 1: Parser

The parser turns CSS source into a typed AST. The entry point is parser2::new_from_str(css_string) -> (Css, Vec<CssParseWarnMsg>), which is non-fatal: a syntax error becomes a warning and the rest of the stylesheet survives. Internally, it tokenises via azul_simplecss::Tokenizer, handles @media / @lang / @theme / @supports blocks, and routes each (key, value) declaration through parse_css_property to a per-property parser in css/src/props/layout/ or css/src/props/style/.

The output Css value is a flat Vec<CssRuleBlock> — each pairing a CssPath selector with a Vec<CssDeclaration> plus an optional list of @-rule conditions and a priority: u8 layer label (UA / SYSTEM / AUTHOR / INLINE / RUNTIME — see rule_priority). Selectors are separately parseable via parse_css_path for runtime StyledDom::with_css(...) overrides and for the codegen pipeline that compiles HTML+CSS to const Rust.

The 180-variant CssProperty enum is the type-safe currency that flows out of the parser into every other stage. Property modules are split by their effect on the layout pipeline (props/layout/ for box-geometry, props/style/ for paint-only, props/basic/ for primitive value types). The split is what enables the RelayoutScope classification on CssPropertyType.

For details, see CSS Parser.

Stage 2: Cascade

The cascade owns the CssPropertyCache. Its job is to take the parsed Css, the per-node inline style (a Css) from NodeData, the global * rules, and the UA defaults — and produce, per node, a fully resolved property set ready for the compact-cache encoder.

StyledDom::restyle(css) is the orchestration entry point. It runs four phases in order:

  1. Match selectors and fill cascaded_props / css_props.
  2. Apply UA-CSS defaults per node_type.
  3. Resolve em / rem / inherited values.
  4. (Caller responsibility) build the compact cache, passing the previous frame's font hashes so dirty-tracking can fire.

The flat arena's pre-order index — parent index < child index — is load-bearing. Every cascade pass walks 0..node_count forwards and trusts that any value read from a parent is already resolved. Inheritance, font-size resolution, and the inheritable-tier-1 mask copy all rely on this.

Pseudo-state changes (hover, focus, active) don't re-run a full restyle. restyle_on_state_change walks only the affected nodes and produces a RestyleResult of deltas, which feeds back through ChangeAccumulator::merge_restyle_result so the rest of the pipeline doesn't care whether a change came from a DOM diff or a hover toggle.

Every CssPropertyType is classified by relayout_scope(conservative: bool) -> RelayoutScope:

pub enum RelayoutScope {
    None,         // repaint only (color, opacity, transform, filter, …)
    IfcOnly,      // re-shape the inline-formatting-context (text-content, font-size)
    SizingOnly,   // recompute this node's sizing (width, height, padding, …)
    Full,         // full subtree relayout (display, position, float, …)
}

Taffy uses a binary clean / dirty flag; the four-level classification lets the layout solver skip subtree walks when only an IFC's text needs reshaping or skip a parent's reflow when only a child's color changed.

For the cascade walk, the inheritable-property mask, the slow-path resolver, and the legacy two-build-paths consolidation, see Cascade, Inheritance, Restyle.

Stage 3: Compact cache

The compact cache is a four-array, fixed-layout encoding of the ~50 layout-hot CSS properties. Layout reads them by node index in O(1) — no BTreeMap lookups, no cascade walks. Built once per restyle by build_compact_cache_with_inheritance; read on every layout pass.

The four arrays are:

  • tier1_enums: Vec<u64> — 21 enum-valued layout properties packed into a single u64 per node (display, position, float, overflow, flex-, justify-, align-*, white-space, direction, ...). 8 B per node.
  • tier2_dims: Vec<CompactNodeProps> — width, height, min/max-*, padding, margin, border-width, top/right/bottom/left, flex-grow/shrink, gap. 68 B per node.
  • tier2_cold: Vec<CompactNodePropsCold> — paint-only and rare-but-typed: border colors, border radii, z-index, border-styles, grid placement, opacity, plus two u8 „has-X“ flag bytes for fast-skip negative paths. 28 B per node.
  • tier2b_text: Vec<CompactTextProps> — text-color, font-family-hash, line-height, letter-spacing, word-spacing, text-indent. The whole struct is inheritable as a unit. 24 B per node.

Total: 128 B per node, 128 KB for a 1000-node DOM.

Properties that don't fit (background, box-shadow, transform, filter, content, transitions, ...) live on the slow CssPropertyCache::get_property_slow path and are not duplicated here. The HOT_FLAG_HAS_* and DOM_HAS_* bits are the negative fast paths: when the bit is clear, the renderer can skip the slow walk for that property entirely.

Sentinel encodings let the same u32 field carry typed data and the auto / none / inherit / initial keywords. The FloatValue ×1000 representation is what makes the encoders work in const context — encoding/decoding uses no floats.

For the bit layouts, the encoder's per-node steps, the sentinel tables, the font-dirty-tracking mechanism, and the procedure for adding a property to a tier, see Compact Property Cache.

System style: where UA defaults come from

User-agent defaults aren't a single hard-coded sheet. They're a SystemStyle value populated at app start from the operating system: theme (light / dark), accent colour, semantic UI colours, system fonts, scrollbar look, double-click time, reduced-motion preference, and more. Every callback's CallbackInfo exposes get_system_style() -> Arc<SystemStyle> so widgets and CSD code can consult it live.

Discovery is per-platform and lives in dll/src/desktop/shell2/<platform>/system_style.rs. macOS uses dlopen + Objective-C, Windows uses LoadLibrary + GetProcAddress for user32.dll and dwmapi.dll, Linux first tries the XDG Desktop Portal over raw D-Bus and then falls back to per-DE CLI helpers (gsettings, kreadconfig5, Hyprland config, pywal cache). Every backend starts by cloning a hard-coded default and mutates fields based on what the OS actually returned, so a query failure for a single value leaves the rest of the style intact.

Beyond UA defaults, SystemStyle carries two CSS-emitting methods (create_csd_stylesheet, create_menu_stylesheet) that build per-app stylesheets at runtime, and an app_specific_stylesheet slot for the user's ~/.config/azul/styles/<exe>.css ricing file.

For the compile-time defaults table, the discovery order, the per-platform priority chains, and the cross-cutting checklist when adding a field to SystemStyle, see System Style Discovery.

End-to-end flow

        user CSS              NodeData.css_props        SystemStyle           runtime overrides
            |                        |                       |                       |
            v                        v                       v                       v
+-----------+                +-------+-----------------------+--------+      +-------+
|  parser2  |  Css   ----->  |          CssPropertyCache              |  +-> | comp  |
|           |  ----->        |   (cascaded + global * + per-node)     |  |   | cache |
+-----------+                +----------+-----------------------------+  |   | build |
                                        |                                |   +---+---+
                                        v                                |       |
                       compute_inherited_values + apply_ua_css_to_compact|       v
                                        |                                |   +---------+
                                        v                                |   | tier1   |
                            build_compact_cache_with_inheritance --------+   | tier2   |
                                                                             | tier2_c |
                                                                             | tier2b  |
                                                                             +----+----+
                                                                                  |
                                                                                  v
                                                                     layout / paint reads

A frame's restyle is one pre-order arena walk. A pseudo-state toggle is restyle_on_state_change, which only touches the affected nodes. A new font-family hash on a text node fires only when tier2b_text[i].font_family_hash differs from prev_font_hashes[i], so font resolution is incremental.

Where to start

  • Adding a CSS property: read CSS Parser first to define the typed value and parser, then Cascade to wire it through parse_css_property and RelayoutScope, then Compact Property Cache if it should be on the fast path.
  • Debugging a „why does my style not apply“ issue: read Cascade to understand the priority order and the slow-path walk in get_property_slow.
  • Adding a system colour or scrollbar metric: read System Style Discovery, then update the per-platform discover() functions and the defaults::* constructors.
  • Reading the encoded value at runtime: the getters on CompactLayoutCache are documented in Compact Property Cache.

See also

  • DOM InternalsNodeData::css_props is one of the cascade's input sources.
  • Layout SolverRelayoutScope classifies what work the layout solver actually does.
  • Rendering Pipelinetier2_cold flags determine which slow-path renderer code paths run.

Coming Up Next

Back to guide index