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.
- 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.
- 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 ...
];
- Make sure the form is in scope.
buildPropGroupTabs()only runs on forms matchingDrupal.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.