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¶
- 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. - 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 acss_varsprofile also clearslayout_mapsandspacing_side_mapoutright (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@sourcefile (v4) orsafelistarray (v3). - Bootstrap 5: Bootstrap ships its utilities in CSS, but responsive color
utilities and custom
$theme-colorsmust 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.