Build a custom Widget
Add a palette item — drag, drop, 30 minutes.
You've built a custom Element (C1) and a custom Control (C2). The third extensibility surface is the Widget — the palette item the buyer drags from the left rail onto the canvas. A widget bundles "the Element class + its preset content + a palette icon" into one drag-droppable thing. Time budget: 30 minutes.
This is the smallest of the three extension surfaces. If your project only needs one new component, a Widget is often the right shape — it's faster to ship than a custom Element + Control combo.
What a Widget IS
A Widget is a class that wraps a pre-built block of content + a palette icon + a label. The buyer drags the icon, drops a fully-formed block onto the canvas. The shipped catalogue has 20+ widgets — Heading, Paragraph, Image, Button, Divider, List, Icon, Social, RSS, …. Each is ≈ 50-100 LOC.
Subclass BaseWidget:
constructor()— buildthis.block, theBlockElement(or any single Element subclass) the buyer drops onto the canvas.getName()— palette label. Routes throughI18n.t('widgets.<key>')for translated builds (see i18n).getIcon()— Material Symbols Rounded icon name (campaign,image,format_list_bulleted, etc.). Browsers render the ligature via the font.
Pre-flight
Confirm the demo runs:
Step 1 — Subclass BaseWidget
Step 2 — Register with the palette
The widgets palette doesn't auto-discover — you call addWidget() after Builder boots. Inside the load callback so the engine has registered its stock widgets first:
- The palette is empty by default unless you call
addWidget()— even for the stock widgets like Heading + Image. The shippeddemo/builder.phpregisters them in its boot block; if you're authoring your own host page, you'll need to register every widget you want surfaced. render()after every batch ofaddWidgetcalls. Otherwise the palette DOM doesn't update until the next mutation.
The group option ('Custom' here) becomes the section heading in the palette UI; the buyer sees groups like "Layout", "Content", "Forms", "Custom".
Step 3 — Per-widget palette polish (optional)
If you want a custom palette icon style (a rounded chip vs the plain icon), use the [widget-class="<ClassName>"] styling hook:
Step 4 — i18n (optional)
If your build supports multiple languages, route the palette label through I18n.t():
Step 5 — Test
- Open the builder. The Callout widget appears under "Custom" in the palette.
- Drag it onto the canvas. The pre-built block lands; the clone is editable.
- Edit the heading. Save. Reload. The edited content survives.
- Drag a second Callout next to the first. Each is independent (no shared state).
If step 1 fails (the widget isn't in the palette), confirm addWidget was called inside the load() callback (or after load()'s promise resolves). If you call it before load() runs, the palette renders empty because widgetsBox doesn't exist yet.
Did you make it?
You should now have:
- ✅ A
CalloutWidgetclass subclassingBaseWidgetwith the 3 hooks (constructor,getName,getIcon). - ✅
this.blockbuilt in the constructor with the prebuilt content. - ✅ Widget registered in the load callback via
widgetsBox.addWidget(new CalloutWidget(), { group: 'Custom' }). - ✅
widgetsBox.render()called after the batch. - ✅ Drag-drop working: dragging the icon drops a clone of
this.blockonto the canvas.
If a step didn't work:
- 🩹 Widget not appearing in palette → it was registered too early (before
load()callback) orwidgetsBox.render()wasn't called. - 🩹 Drag does nothing → confirm
widgetsContainerconstructor option points at your real DOM element. Ifnull, the widgets box doesn't render at all. - 🩹 Drop creates a block but the heading text is wrong → confirm your
this.block'sHeadingElementconstructor received the right args. The engine mostly takes the constructor verbatim. - 🩹 Palette items load but no styling on your custom one → use the
[widget-class="…"]selector, not a class on the widget element directly (the engine's DOM ships withwidget-classas an attribute).
What's next?
- → Custom overlays (advanced) — per-element on-canvas affordances (resize handles, drag indicators, image crop).
- 🔗
examples/extensions/1-custom-widget/— the runnable Callout end-to-end. - 🔗
src/includes/BaseWidget.js— the parent class. - 🔗
src/includes/HeadingWidget.js+ParagraphWidget.js— engine-shipped widgets for reference.
Stuck?
- 🐛 grep
src/includes/<Name>Widget.js— the catalogue of shipped widgets is your library of working examples. - 🐛 file an issue with: your CalloutWidget class, the boot block calling
addWidget, console errors.