Framework profiles¶
Switch between Bootstrap, Tailwind, and the built-in Custom engine.
Overview¶
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), rendered by default 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). This is 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, and wider columns hold the responsive infix/prefix variant.
gap_classes is a legacy standalone gap table: the settings form no longer writes it, and gap resolves from layout_maps.<component>.gap instead. It remains in the schema and in profile snapshots only for backward compatibility with existing saved profiles.
| 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, so 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 and columns.reverse have no class family. Both resolve
per-viewport via CSS custom properties (--cb-columns-count-{breakpoint},
--cb-reverse-{breakpoint}), which one class per enum value cannot express.
A class could only ever describe the base breakpoint, and the generated
cascade CSS overrode it anyway. 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 wins, not the
CSS-var fallback. 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), so 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.
Export the safelist (the panel above, or getSafelistClasses() /
the Drush one-liner in
Tailwind → Export the safelist) and feed
it to your build as a Tailwind @source file (v4) or safelist array (v3).
Bootstrap ships its utilities in CSS, but responsive color
utilities and custom $theme-colors must be opted in at SCSS build time. See
Bootstrap. The class tables tell you which utilities/colors
your build must include.
Layout renders from CSS custom properties and the
cb-p-*/cb-m-* spacing utilities the module emits itself, so there's 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
for the SCSS opt-in) or you should replace the rows with your own utility names.
