Skip to content

Canvas Builder — Framework Profiles

Canvas Builder can be re-skinned for different CSS frameworks without changing the component model. The settings UI is split into three tabs: General, Styles, Components.

Two styling subsystems

  1. Style plugins (Background / Spacing / Typography) — framework-agnostic class-string tables. Each option stores one CSS class per viewport ({label, classes:{viewportId: class}}). Any framework works. Edited on the Styles tab.
  2. Layout components (the Section SDC and its internal Column child; the Columns/Grid layout modes both render on .cb-columns) — by default rendered with CSS custom properties (--cb-columns-count, etc.). This is the Custom engine: fully framework-independent. Edited on the General tab.

Shipped profiles

Three profiles ship with the module and cannot be deleted:

ID Engine Description
custom css_vars Default. Layout via CSS custom properties.
bootstrap5 classes Bootstrap layout maps, gap-* gap (not g-* — the gutter utilities are inert on CSS grid), responsive color utilities.
tailwind classes Tailwind layout maps (grid-cols-*, flex utilities), gap-*, starter tables.

Profile class data ships in src/Service/ProfileData/{id}.yml — static YAML files loaded at runtime, not Drupal config. They never appear in drush cex output.

User profiles are supported by the API (FrameworkProfileManager::saveLiveConfigAsProfile()) and stored in canvas_builder.settings, but there is no UI to create one in 1.0.0-alpha3 — only the three shipped profiles are selectable.

How profiles work

A profile is a saved snapshot of the full class-config surface (style tables, gap options, grid min-widths, layout maps, the box-model side→letter map) plus a render engine.

  • Switch framework (General → Framework → Use this framework as my base) — non-destructive: the current live keys are auto-snapshot back into the active profile's config, then the target profile's snapshot is loaded. For shipped profiles on a fresh install, the snapshot comes from src/Service/ProfileData/ since no config snapshot exists yet. Switching to a css_vars profile also clears layout_maps and spacing_side_map outright (they cannot be correct for a CSS-variable engine); the auto-snapshot into the outgoing profile runs first, so nothing is lost.

The active engine is exposed as drupalSettings.canvasBuilder.activeEngine. Component HTML carries the config:canvas_builder.settings cache tag so a profile switch re-renders it.

Layout class maps

layout_maps translate each layout SDC enum value to a framework class per viewport: component → family → value → {viewportId: class} (e.g. columns.gap.lg.tablet = gap-md-4).

Each matrix cell holds one breakpoint variant — mobile holds the un-prefixed class; wider columns hold the responsive infix/prefix variant.

Viewport Bootstrap 5 infix Tailwind prefix
Mobile (none) (none)
Tablet -md- md:
Desktop -lg- lg:
Large Desktop -xl- xl:

Single-cell families. section.container_type ships base-only — container or max-w-screen-lg is already a responsive rule, stacking per-breakpoint variants would be wrong. section.height is likewise base-only, and only the class-mappable modes (auto, full) are rows — Min/Fixed height render as inline min-height/height and have no class to map.

columns.count is not emitted. The column count is per-viewport (--cb-columns-count-{breakpoint}), which one class per enum value cannot express — the class could only ever describe the base breakpoint, and the generated cascade CSS overrode it anyway. The matrices stay in config so existing customisations survive; use the Columns defaults to control counts.

Bootstrap misfits. section.height's fit row has no Bootstrap fit-content utility and stays blank (the structural cb-section--h-fit class carries it). section.container_type is base-only but is mapped (container / col-lg-8 mx-auto / w-100).

Blank cell = no class at that breakpoint. The renderer emits nothing; the CSS-grid engine fallback applies.

Alignment families (section.halign, columns.valign/columns.halign, grid.grid_valign/grid.grid_halign) are emitted as real, per-viewport, mode-resolved classes under a classes engine — not just CSS variables. columns.* and grid.* both render onto .cb-columns, but only one layout mode is active at any given viewport; the renderer resolves the active mode per viewport and emits only that mode's family, gating the generated cascade CSS (align-items/justify-content/justify-items) behind a data-cb-*-engine="classes" attribute so the utility class — not the CSS-var fallback — wins. A family that resolves to an empty class at any required viewport (e.g. Bootstrap's grid.grid_halign, which has no map at all — see below) is abandoned as a whole and falls back cleanly to the --cb-* custom property, with no gate attribute stamped. align-content is the one property that never gets a class channel — no layout_maps family maps to it and it stays var-driven under every engine.

Columns/Grid always use CSS-grid

Columns and Grid deliberately use the framework-agnostic CSS-grid engine under every profile. Bootstrap's flexbox grid (.row/.col) requires per-child classes, which fights Canvas's slot model and direct-child selectors. CSS Grid already expresses every feature wrapper-only. The columns.* layout maps serve the Tailwind safelist export and are available as optional wrapper-class overlays.

Tailwind safelist

Because Canvas Builder applies classes from config at runtime, Tailwind's purge cannot detect them statically. Go to General → Framework → Tailwind Safelist to get a copy-paste list of every currently mapped class token (style tables + layout maps), filtered to enabled viewports. It also includes the per-side spacing names the box-model editor derives at runtime (pt-/pr-/pb-/pl-/px-/py-, or Bootstrap's logical pe-/ps-), which appear in no source file.

For the programmatic API: FrameworkProfileManager::getSafelistClasses().

Exporting & importing classes

There are two distinct "classes" to move, depending on what you need:

1. The class config (portable between Drupal sites). Your style tables, gap options, grid min-widths, layout maps and any user-created profiles live in the canvas_builder.settings config object. Move them with standard config sync:

drush config:export        # writes canvas_builder.settings.yml to your sync dir
# … commit / copy to the target site, then on the target:
drush config:import

The three shipped profiles (custom, bootstrap5, tailwind) are code, not config (src/Service/ProfileData/*.yml) — they are never in drush cex output. Only your live (active-profile) class tables and any profiles you saved travel in config.

2. The compiled CSS (into your theme's build). Exporting config does not produce CSS — the framework still has to generate rules for those class names:

  • Tailwind: export the safelist (the panel above, or getSafelistClasses() / the Drush one-liner in TAILWIND.md → Export the safelist) and feed it to your build as a Tailwind @source file (v4) or safelist array (v3).
  • Bootstrap 5: Bootstrap ships its utilities in CSS, but responsive color utilities and custom $theme-colors must be opted in at SCSS build time — see BOOTSTRAP.md. The class tables tell you which utilities/colors your build must include.
  • Custom (css_vars): layout renders from CSS custom properties and the cb-p-*/cb-m-* spacing utilities the module emits itself — nothing to export for those. But the shipped color and text-align tables use Bootstrap-style names with responsive infixes (bg-primary, bg-md-primary, text-lg-white): either your theme must supply those classes (see BOOTSTRAP.md for the SCSS opt-in) or you should replace the rows with your own utility names.