Skip to content

Prop groups & widgets

Mount your own tab or widget outside the style system.


Overview

Three extension points outside the style system entirely: mounting your own tab into a component form, the slot-recommendation annotation behind the editor's one-click "add", and changing how a component's own prop renders. None of these need a StyleInterface implementation; see Extending styles and Custom style types for that system.


1. Adding a prop-group tab (a peer to Content/Style/Advanced)

The built-in Content/Style/Advanced tab strip (assets/components/prop-groups/prop-groups.js, buildPropGroupTabs()) is generic: it converts any set of sibling <div data-canvas-prop-group="id"> panels inside a form into an ARIA tab UI, driven by the ordered list at drupalSettings.canvasBuilder.propGroups. Other modules that mount their own panel into that same form (rather than adding a style) can register a new tab without touching canvas_builder. This is exactly how canvas_builder_ai's "Create" tab, and the "Create"/"Details" split on the Page-data form, work.

  1. Register the group with hook_canvas_builder_prop_groups_alter():
/**
 * Implements hook_canvas_builder_prop_groups_alter().
 */
function my_module_canvas_builder_prop_groups_alter(array &$groups, array $context): void {
  $groups['my_tab'] = [
    'id' => 'my_tab',
    'title' => t('My tab'),
    'weight' => 30,
    'svgPath' => '<path fill="currentColor" d="M12 2 2 12l10 10 10-10z"/>',
  ];
}

$context carries caller-supplied hints (canvas_builder passes ['form_id' => 'component_instance_form']); use it if your tab should only apply to specific forms; otherwise register unconditionally and rely on step 2, since a group with no matching panel on a given form is simply unused there.

  1. Wrap your panel in a matching hook_form_FORM_ID_alter():
$form['my_panel'] = [
  '#type' => 'container',
  '#attributes' => ['data-canvas-prop-group' => 'my_tab'],
  // ... your render array ...
];
  1. Make sure the form is in scope. buildPropGroupTabs() only runs on forms matching Drupal.canvasBuilder.FORM_SELECTOR (assets/core/core.js), currently the component-instance form (component_instance_form / component-instance-form) and the page-data form (page_data_form / canvas-page-form). If you're mounting into a different form, extend that selector too.

A panel with no real content (all its fields relocated elsewhere by another plugin, or empty because a runtime condition isn't met) is automatically excluded from the tab bar rather than showing an empty tab; see hasVisibleContent() in prop-groups.js. To make a panel's applicability dynamic (show the tab only when some client-side condition holds, e.g. a particular type of component is selected), apply the same clip-hidden-wrapper signature that function already scans for (position: absolute plus a literal ≤2px width/height) to your panel's content when inapplicable, then call Drupal.canvasBuilder.refreshPropGroupTab(form, 'my_tab') to have the tab bar re-evaluate immediately, needed for any state change that doesn't trigger a fresh form fetch. See canvas_builder_ai's panel.js (setApplicable()) for a worked example.


2. Declaring slot recommendations (the one-click "add" affordance)

A component can tell the editor which child belongs in each of its slots, via a top-level x-canvas-builder: key in its own *.component.yml:

x-canvas-builder:
  slots:
    slides:
      recommended:
        - acme_blocks:hero_banner_slide
      label: 'Add Slide'

recommended is a list of component ids; either spelling works (acme_blocks:hero_banner_slide or sdc.acme_blocks.hero_banner_slide); Slot\SlotRules normalises everything to the sdc. form internally. An id that doesn't resolve to an installed SDC is dropped silently, and a malformed annotation (a scalar x-canvas-builder, or a non-array slots) degrades to no rules at all rather than throwing. label is optional (falls back to "Add {Component label}"); enforce is parsed and stored but nothing currently acts on it in the editor; don't rely on it to block other children.

Naming-convention fallback. A component with exactly one slot and no usable x-canvas-builder declaration gets a companion child inferred by name, probed in this order: {id}_slide, {id}_item, {singular(id)}_item, {id}_child (singularisation just strips a trailing s). This is why hero_banner + hero_banner_slide, or tabs + tab_item, work with no YAML at all. The fallback only applies when the x-canvas-builder key is genuinely absent; if it's present but unusable, the component gets no rules and no convention guess (author error is not silently second-guessed).

Editor effect. The floating badge over a container with a slot rule gets a [+] button that inserts the recommended child in one click, including into an empty slot. It targets the first slot that has a rule, not simply the component's first slot.

AI effect. canvas_builder_ai's ComponentInventory uses this same service for its slot allowlist. A component can narrow that further with its own x-canvas-builder-ai: slots: declaration, which takes precedence over this contract wherever both are present; the two are not merged.


3. Themed prop widgets (x-canvas-widget)

Any component's own prop (not a canvas_builder style) can render as a themed widget instead of Canvas's plain <select>/number input. Declare x-canvas-widget: inside the prop's JSON schema in your *.component.yml; this works for any SDC, not just canvas_builder's own. The value stays a normal component prop: it persists through Canvas's own auto-save, not canvas_builder's style system, so there's nothing else to wire up.

Every widget below (except range) enhances an enum prop: Canvas already renders it as a <select>; the widget just replaces how it looks. Use "meta:enum" for human-readable labels; it's what the widgets read for button text and swatch tooltips.

x-canvas-widget Renders as Needs
segmented Text button strip
icon-group Icon button strip x-canvas-widget-iconset
card-group Icon + title + description pill tray x-canvas-widget-iconset (optional), x-canvas-card-text (optional)
range Slider stepping through the enum's ordered values
color-swatch Grid of color/gradient swatches enum values must themselves be CSS color/gradient strings
cols-preset Section's column-width-ratio picker purpose-built for ratio strings like 2-1-1; not intended for other props
alignment:
  type: string
  enum: [start, center, end]
  "meta:enum":
    start: Start
    center: Center
    end: End
  default: start
  x-canvas-widget: segmented

x-canvas-widget-iconset: foo looks up foo-{value} in the icon registry (drupalSettings.canvasBuilder.icons, populated from src/Icons.php) for each option; missing icons fall back to the label.

text_align:
  type: string
  enum: [left, center, right]
  "meta:enum":
    left: Left
    center: Center
    right: Right
  default: left
  x-canvas-widget: icon-group
  x-canvas-widget-iconset: text-align
container_type:
  type: string
  enum: [contained, full]
  "meta:enum":
    contained: Contained
    full: Full width
  default: contained
  x-canvas-widget: card-group
  x-canvas-widget-iconset: container
  x-canvas-card-text:
    contained: 'Content stays within the max-width'
    full: 'Content stretches edge to edge'

Options render as ordered slider steps, not a numeric input — good for a small enum like a t-shirt-size scale.

gap:
  type: string
  enum: [none, sm, md, lg, xl]
  "meta:enum":
    none: None
    sm: Small
    md: Medium
    lg: Large
    xl: X-Large
  default: md
  x-canvas-widget: range

Unlike the others, the enum's own values ARE the rendered colors: literal hex/rgb/gradient CSS strings, applied directly with no class-probing. Use "meta:enum" for the swatch's hover-tooltip label.

brand_color:
  type: string
  enum: ['#e63946', '#2a9d8f', 'linear-gradient(135deg, #ff6a00, #ffd200)']
  "meta:enum":
    "#e63946": Cardinal
    "#2a9d8f": Teal
    "linear-gradient(135deg, #ff6a00, #ffd200)": Sunset
  default: '#e63946'
  x-canvas-widget: color-swatch

A different mechanism from canvas_builder's own background/overlay color pickers (canvas_builder/plugin.color_swatch), which pick a utility class (e.g. bg-primary) and probe the live theme CSS to resolve its color. color-swatch has no class to probe; the value the author picks is the value stored on the prop.

Purpose-built for Section's own column-width-ratio strings (2-1-1); not intended for other props, so there's no example to copy here.