A dynamic dark mode renderer does not replace a webpage or paint a dark copy over its screenshot. It reads the page’s CSS, decides which values behave as backgrounds, text, borders, shadows, gradients, or images, and writes a parallel set of override rules. The website keeps its structure and behavior; the generated color layer stays attached as that structure changes.

The renderer also has to work in two tempos. A fast fallback covers the bright page before full analysis is ready. A slower, more precise pass collects style information, generates rules, handles asynchronous image work, and keeps watching for later changes.

Diagram showing a dynamic dark mode renderer collecting CSS, classifying declarations, mapping colors, injecting overrides, and watching page changes
The dynamic CSS pipeline used by the current rendering path. The original page remains in place while generated styles are built and maintained beside it. Diagram reviewed against the renderer source on July 13, 2026.

The first dark surface is intentionally provisional

The renderer is injected at document_start, but the page’s useful CSS may not exist yet. External style sheets are still loading, framework code has not mounted its components, and custom elements may not have attached a shadow root. Waiting for all of that work produces a white flash. Pretending the final palette is already known produces the wrong first frame.

The renderer begins with a fallback style generated from the selected theme. It gives the document, body, borders, and text a safe dark baseline while excluding or limiting treatment around frames that may manage their own document. A separate user-agent layer covers standard links, tables, placeholders, form controls, selection, scrollbars, and autofill states.

The fallback is deliberately broad. Its purpose is to make the page readable during startup, not to preserve every component boundary. As real style sheets become available, precise generated rules replace that provisional treatment. This is the central first-paint tradeoff: fast coverage now, accurate hierarchy a little later.

Collection starts with every manageable source of CSS

An old-fashioned page may have one linked style sheet. A current web application can combine linked files, inline <style> blocks, element style attributes, constructed style sheets, adopted style sheets, nested @media and @layer rules, imports, and rules inserted by JavaScript.

The renderer creates a style manager for each source it can read. For ordinary style elements and accessible linked sheets, it works with the browser’s CSS rule objects. If a linked sheet cannot be read directly through CSSOM, the manager can request its text through the extension, remove comments, resolve imports, and convert relative asset URLs to absolute ones before creating a local representation. A sheet that is still loading is marked for a second pass rather than assumed to be empty.

Inline styles take another path because there is no reusable selector rule to copy. The renderer watches relevant style attributes, stores the source value, and writes transformed values through its own custom properties. Constructed and adopted style sheets need their own manager and a page-world proxy so changes made through methods such as replace, insertRule, or deleteRule remain visible to the extension.

This collection phase is one reason dynamic rendering is not equivalent to adding a dark class to <html>. The site did not author a complete dark variable set for the extension to toggle. The extension has to understand the color-bearing declarations the site already shipped.

From a CSS value to a generated rule

Once the renderer has collected the page’s style sources, each declaration follows the same broad path: identify its visual role, map the source value into the chosen palette, preserve its CSS context, and emit an override that can be replaced later.

A color’s property reveals its likely role

The same source value can mean different things. #777 used for body text should not receive the same mapping as #777 used for a panel background or a one-pixel divider. Dynamic rendering begins with the CSS property and surrounding declaration, not with the color alone.

The renderer groups modifiable values into several practical roles:

  • Background colors and gradient stops.
  • Foreground text, SVG fill, stroke, and related color properties.
  • Borders and outlines.
  • Box and text shadows.
  • Background and list images that require a separate decision.
  • Browser color-scheme, scrollbar, control, and selection values.

A masked element is a good example of why property names are only the start. A background color beneath a nontrivial mask often behaves visually like foreground artwork, so the renderer treats it as a foreground rather than a page surface. Border shorthands also need inspection: a declaration that resolves to no visible border should not generate a colored edge merely because its property name begins with border.

The resulting classification is not a claim that the engine has discovered the designer’s semantic component model. It is a narrower inference: this value is being used in a foreground-like, background-like, or boundary-like position, so it needs the corresponding color transform.

Theme anchors guide a mapping rather than a replacement

Each theme supplies background and text anchors along with brightness, contrast, grayscale, and sepia controls. The renderer does not replace every background with the background anchor and every foreground with the text anchor. That would collapse the page into two colors.

Instead, the source color is converted to a perceptual representation and remapped within the destination range for its role. Light neutral backgrounds move toward the selected background pole. Dark foregrounds move toward a readable text range. Saturated link and accent colors keep more of their hue, with adjustments where a hue would become difficult to read on the new surface. Borders receive their own mapping so they remain quieter than text but visible between adjacent layers.

The distinction is visible in the Classic Midnight Wikipedia field test. The article background, notice, table cells, dividers, links, and text do not all become the same two hex values. They retain enough separation to describe the original interface.

Repeated transformations are cached using the source RGBA value and the theme parameters. If the same source gray appears in hundreds of rules, its transformed result does not need to be recalculated hundreds of times. The renderer also registers generated colors in a page palette so later rules can reuse the same values consistently.

This is the technical difference behind CSS Filter vs Dynamic Theme: a filter transforms the composed pixels, while the dynamic engine transforms color-bearing declarations according to their likely job.

CSS variables require a dependency graph

Custom properties make modern design systems convenient because one value can flow through an entire application. They also hide meaning from a renderer. The declaration --brand: #2563eb does not reveal whether the variable will become text, a border, a gradient stop, or all three.

The renderer first records variable definitions and references, then observes where each variable is consumed. A variable used by background-color is marked as a background color. One used by color is a foreground. Border and outline use add another type. A variable used inside background-image may be a color or an image expression, so its dependencies must be followed before the correct modifier is known.

One source variable can need more than one generated value. If --brand is used for both a blue link and a pale selected background, the renderer creates role-specific wrapped variables rather than forcing one dark result into both contexts. References are rewritten to the matching generated name. Fallback values inside var() are transformed through the same role-aware function.

This classification can change after startup. A framework may define a variable before any component reveals how it is used, or insert a new rule that gives an existing variable another role. The variable store keeps subscriptions for those changes and rebuilds affected declarations when the type becomes known. That is more work than string replacement, but without it a variable-based site would drift out of theme as components load.

Generated rules preserve the original selector structure

The renderer does not edit the author’s sheet in place. For each manageable sheet it creates an override sheet, walks the source rules, and collects only declarations that need modification. The source selector is retained, as are relevant grouping rules such as media queries and layers. Generated declarations preserve !important where the source used it.

Each source rule receives a hash based on its CSS text and grouping context. The style manager remembers the corresponding modifiable rule. On a later pass, unchanged rules can reuse their parsed form; removed rules are dropped from the caches. A theme-key cache separately detects when the page stayed the same but the user selected another palette.

Some declarations finish asynchronously. Background-image analysis may require fetching and sampling an asset, while a CSS variable may not have enough information until another rule appears. The override sheet can render the declarations that are ready, then rebuild the affected rule when the promised value resolves. Asynchronous work is queued and cancelled when a newer render has made its result stale.

This parallel-sheet design gives the site room to continue operating. Its JavaScript still owns its style sheets and can change them. The extension owns the generated layer and can remove it cleanly when it pauses or detects a native dark theme.

A live page turns rendering into an update loop

After the initial pass, the renderer watches for new, removed, disabled, or reordered style elements; changes inside CSS rule lists; inline style mutations; and newly discovered shadow roots. Custom elements that are not defined yet are revisited when their implementation becomes available. Adopted style sheets are tracked for both document and shadow-root scopes.

The observers do not immediately rebuild the entire page for every mutation. Style changes are batched, repeated work is throttled or scheduled on animation frames, and each manager decides whether its own source actually changed. Hidden documents can pause expensive activity and restore it when visible again.

Single-page navigation is therefore not a special second renderer. The same managers notice that a route inserted new elements and styles, then generate only the missing or changed layer. Safari page restoration adds another surrounding lifecycle, covered in How Safari Dark Mode Extensions Work.

Images and site fixes branch away from ordinary color

A gradient is still CSS color and can be parsed stop by stop. A URL inside background-image is different. It may point to a photograph, transparent icon, texture, logo, or a nearly solid bitmap standing in for a color. The renderer sends that case through an asynchronous image path and can add a small filter, keep the original, or substitute a generated solid result when the evidence supports it.

Media decisions are deliberately conservative because the cost of a wrong transform is high. A dark icon disappearing on a dark card may need help; a product photograph should not have its colors “corrected” into another product. How Dark Mode Extensions Protect Images and Video covers that branch and its cross-origin limits.

The general renderer also accepts site-specific dynamic fixes. A rule can invert, keep unchanged, or ignore image analysis for particular selectors; change the root style; ignore inline values; or provide CSS for a known compatibility issue. These are patches around the general algorithm, not a second full theme written for every domain. The narrower the selector and reproduced failure, the safer the fix remains after a website update.

What dynamic rendering can and cannot promise

Dynamic rendering preserves more page meaning than a blanket filter because it can distinguish roles and leave media alone. It also carries more moving parts: CSS parsing, dependency tracking, asynchronous assets, observer scheduling, cache invalidation, browser boundaries, and site exceptions.

It cannot read a closed shadow root as though it were ordinary DOM. It cannot infer the semantics of every canvas or bitmap. A cross-origin style or frame may be available only through the extension’s permitted fetch and injection paths. New CSS syntax can appear before the parser understands it. A site can also use color as data, where any automated remapping is the wrong choice.

The useful promise is narrower. For ordinary web CSS, a dynamic renderer can build a role-aware dark layer, keep that layer synchronized with a changing page, and remove it without replacing the site itself. It is not a one-time recolor; it is a maintained interpretation of the page’s current style system.