Skip to content

Architecture

How a style is stored, resolved, and rendered.


Overview

How a style is stored, resolved, and rendered. For the invariants, the gotchas, and the history behind them, see CLAUDE.md at the repository root — the internal engineering guide this page is distilled from.

Where styles live

A component's style bundle ({classes, media, bgActiveTab}, keyed by component UUID) exists in two places that are not interchangeable:

  1. Authoritative — the canvas_builder_styles base field on the canvas_page entity. Classes are stored as per-viewport maps ({mobile: 'p-0', desktop: 'p-lg-0'}). Canvas's auto-save draft is forked from this field; every editing read and write goes here.
  2. Derived — Drupal State, written only on publish and read only by the front-end renderer. Flattened class-union strings, plus a few per-viewport maps for keys that must resolve per breakpoint at render time (overlays, background color, the active background-type tab).
flowchart LR
    A[Editor changes a style control] --> B[Draft: canvas_builder_styles field]
    B -- publish --> C[Drupal State]
    B -- read for editor preview --> D[Editor canvas]
    C -- read for front end --> E[Published page]

Reading the flattened State store when you need a per-viewport value is the single most common style bug in this codebase — per-viewport detail is lost there by design.

The style plugin system

A style definition (Background, Spacing, Typography, or a contrib addition) is a YAML-shaped array with a type that dispatches to a StyleInterface handler (select, media, group, or a custom type). Definitions merge from three sources, in order: the module's own canvas_builder.plugins.yml, every enabled module's hook_canvas_builder_styles() return value, and every enabled module or theme's {provider}.canvas_builder_styles.yml file — then hook_canvas_builder_styles_alter() runs last, on top of everything. See Extending for the full mechanics and worked examples.

flowchart TD
    P1[canvas_builder.plugins.yml] --> M[Merged definitions]
    P2[hook_canvas_builder_styles per module] --> M
    P3["{provider}.canvas_builder_styles.yml per module/theme"] --> M
    M --> A[hook_canvas_builder_styles_alter]
    A --> F[StyleManager attaches the Style tab]
    F --> G[Each control dispatches to its type handler: select / media / group / custom]

Persist → render pipeline

  1. Build. hook_form_component_instance_form_alter calls StyleManager::attachStyles(), which builds one form element per enabled definition, each carrying a #canvas_style property naming its storage kind and key.
  2. Persist. An AJAX-triggered #element_validate callback (StyleManager::persistElement) resolves the active viewport and writes the submitted value into the auto-save draft via Canvas's own AutoSaveManager::saveEntity(). A value equal to its declared default is not stored, so the field only holds real overrides.
  3. Preview. The editor reads the draft (with a per-request cache) and paints the live preview from data-cb-values per-viewport data attributes — never from the currently displayed control value, which can be stale until a viewport switch re-syncs it.
  4. Publish. Saving the canvas_page entity flattens the draft into Drupal State and invalidates the component's cache tag.
  5. Render. The front-end render callback (CanvasBuilderTrustedCallbacks) reads State, splits any multi-viewport union string into individual sanitized class tokens, and emits them on the component wrapper — plus one background/overlay <div> per contiguous visibility window, for cases where an image or overlay differs across breakpoints.
sequenceDiagram
    participant Editor
    participant Form as StyleManager
    participant Draft as Auto-save draft
    participant State as Drupal State
    participant Page as Published page

    Editor->>Form: change a style control
    Form->>Draft: persistElement() writes per-viewport value
    Draft-->>Editor: live preview repaints from data-cb-values
    Editor->>Draft: publish the page
    Draft->>State: flatten + write derived stores
    State->>Page: render wrapper classes + media layers

Framework profile swap

A profile is a snapshot of the whole class-config surface (every style table, layout_maps, the box-model side→letter map) plus a render engine id (css_vars for the default Custom profile, classes for Bootstrap 5 and Tailwind). Switching profiles is non-destructive: the current live config is auto-snapshotted back into the outgoing profile before the target profile's snapshot loads. The three shipped profiles' data lives in src/Service/ProfileData/*.yml — code, not config, so it's never in drush cex output; a user-saved profile lives in canvas_builder.settings config instead.

flowchart LR
    Live[Live class config] -- auto-snapshot --> Outgoing[Outgoing profile]
    Shipped["src/Service/ProfileData/{id}.yml"] -- or --> Target[Target profile snapshot]
    UserSaved["canvas_builder.settings (user profile)"] -- or --> Target
    Target -- load --> Live

Section and Column layout (count, gap, alignment, height) always renders through CSS Grid, independent of the active engine — a framework's own utility classes ride alongside that layout, they never replace it. See Frameworks.

Where to go deeper