Skip to content

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.

Admin settings page with Custom, Bootstrap 5, and Tailwind framework radio options

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), 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 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, 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.