CSS Engine

browsy includes a CSS engine built from scratch in Rust. It handles selector matching, property parsing, variable resolution, calc() expressions, @media queries, and specificity ordering. The engine computes the subset of CSS properties needed for layout -- approximately 40 properties that affect bounding box computation.

Architecture

HTML ──> DomNode tree
          │
          ├── <style> blocks ──> parse_stylesheet() ──> Vec<CssRule>
          ├── External <link> CSS ──> fetched + parse_stylesheet()
          ├── Inline style="" ──> parse_inline_style_with_vars()
          │
          └── compute_styles() ──> StyledNode tree (LayoutStyle per node)
                │
                └── Taffy layout ──> bounding boxes

Style computation walks the DOM tree, matching each element against all CSS rules by specificity. Inline styles override stylesheet rules. CSS custom properties (--var) inherit through the tree.

Selector matching

The selector engine supports these selector types:

SelectorExampleDescription
Tagdiv, buttonMatches element tag name
Class.nav-itemMatches class attribute
ID#headerMatches id attribute
Universal*Matches any element
Descendantdiv pMatches p inside any div ancestor
Childdiv > pMatches p that is a direct child of div
Pseudo-class:hover, :first-childParsed but ignored for layout (no interaction state)
Attribute (exists)[disabled]Element has the attribute
Attribute (exact)[type="submit"]Attribute equals value
Attribute (word)[class~="active"]Whitespace-separated word match
Attribute (prefix)[href^="/"]Attribute starts with value
Attribute (suffix)[src$=".png"]Attribute ends with value
Attribute (contains)[class*="btn"]Attribute contains substring
Attribute (hyphen-prefix)[lang|="en"]Exact match or prefix with hyphen
Comma-separatedh1, h2, h3Union of selectors

Specificity

Selectors are ordered by CSS specificity rules:

  • ID selectors: weight 100
  • Class selectors, attribute selectors, pseudo-classes: weight 10
  • Tag selectors, universal: weight 1

Higher specificity rules override lower specificity rules. Equal specificity resolves by source order (later wins). Inline styles always win over stylesheet rules.

Property parsing

Supported properties

The engine parses approximately 40 layout-affecting CSS properties:

CategoryProperties
Box modeldisplay, box-sizing, width, height, min-width, min-height, max-width, max-height
Spacingmargin (+ sides), padding (+ sides), border-width (+ sides)
Positionposition, top, right, bottom, left
Flexboxflex-direction, flex-wrap, flex-grow, flex-shrink, flex-basis, align-items, align-self, justify-content, gap
Gridgrid-template-columns, grid-template-rows, grid-column, grid-row
Typographyfont-size, line-height
Visibilityvisibility, overflow

Shorthand properties are expanded: margin: 10px 20px expands to margin-top, margin-right, margin-bottom, margin-left. Similarly for padding, border-width, flex, and gap.

Dimension types

#![allow(unused)]
fn main() {
pub enum Dimension {
    Px(f32),           // Absolute pixels
    Percent(f32),      // Percentage of parent
    Calc(f32, f32),    // calc() result: (px_component, percent_component)
    Auto,              // Auto sizing
}
}

The engine resolves em values against the element's computed font-size and rem values against the root font size (16px default).

var() resolution

CSS custom properties are collected during style computation and inherited through the DOM tree:

:root {
  --primary-color: #333;
  --spacing: 16px;
}

.container {
  padding: var(--spacing);
  color: var(--primary-color);
}

.card {
  margin: var(--spacing-large, 24px);  /* fallback value */
}

The var() resolver supports:

  • Simple references: var(--name)
  • Fallback values: var(--name, fallback)
  • Nested var() references in fallbacks

calc() expressions

The calc() parser handles full arithmetic expressions with mixed units:

.element {
  width: calc(100% - 32px);
  margin: calc(16px + 1em);
  padding: calc(2 * var(--spacing));
}

Supported operators: +, -, *, /. The parser respects operator precedence and handles parenthesized sub-expressions. Mixed px and % units are preserved as a Calc(px, percent) dimension and resolved during layout.

@media queries

The engine evaluates @media queries against the viewport dimensions provided at parse time:

@media (max-width: 768px) {
  .sidebar { display: none; }
}

@media screen and (min-width: 1024px) {
  .container { max-width: 1200px; }
}

Supported media features

FeatureExampleDescription
min-width(min-width: 768px)Viewport width >= value
max-width(max-width: 1024px)Viewport width <= value
min-height(min-height: 600px)Viewport height >= value
max-height(max-height: 900px)Viewport height <= value
width(width: 1920px)Exact viewport width
height(height: 1080px)Exact viewport height
orientation(orientation: portrait)Portrait or landscape
screenscreenAlways matches
printprintNever matches
allallAlways matches

Multiple conditions joined with and are evaluated conjunctively. The screen and / all and prefix is stripped before evaluating conditions.

External stylesheets

When using the fetch feature (enabled by default), browsy automatically fetches external CSS linked via <link rel="stylesheet"> tags. Fetched CSS is parsed and merged with inline <style> blocks during style computation.

Resource limits prevent abuse:

  • Maximum total CSS bytes (across all external stylesheets)
  • Maximum bytes per individual stylesheet
  • Blocked URL patterns (analytics, tracking, ad-related CSS)
  • Private network and non-HTTP URL blocking

Layout engine

After style computation, browsy feeds the styled tree into Taffy (from the Dioxus project) for layout computation. Taffy handles:

  • Flexbox: All flex container and flex item properties
  • CSS Grid: Template columns/rows, explicit placement
  • Block layout: Standard block flow with margins, padding, borders

Taffy returns bounding boxes (x, y, width, height) for every element, which browsy uses to build the Spatial DOM.

What is NOT supported

The CSS engine focuses on properties that affect element position and size. The following are intentionally not implemented:

  • Visual properties: color, background, border-color, border-radius, box-shadow, opacity, z-index
  • Transforms: transform, translate, rotate, scale
  • Animations: animation, transition, @keyframes
  • Pseudo-elements: ::before, ::after, ::placeholder (no content generation)
  • Advanced selectors: :nth-child(), :not(), ~ (general sibling), + (adjacent sibling)
  • Advanced grid: grid-auto-flow, grid-auto-rows, named grid areas, minmax() in some contexts
  • Columns: column-count, column-width
  • Table layout: table-layout, border-collapse

These omissions are by design. browsy computes where elements are and how large they are, not what they look like. The Spatial DOM output contains position and size data; color and visual styling are irrelevant for agent interaction.