Pagebuilder Widgets
A widget is a pre-configured frontend element that editors place on pages in the Visual Pagebuilder. Widgets ship their own Vue components for rendering, an admin form menu, and optionally a PHP data loader for dynamic content. This page explains the concept and contrasts it with admin plugins.
Widget vs. admin plugin
| Aspect | Admin plugin | Pagebuilder widget |
|---|---|---|
| What | Module in the backend menu | Building block for Pagebuilder pages |
| Directory | _public/extensions/core/backend/{plugin}/ | _public/extensions/core/backend/{plugin}/widgets/{widget}/ |
| Class | {plugin}_BackendPlugin extends install_controller | {widget}_PageBuilderPlugin (no inheritance) |
| Registration | $this->content_construct + $this->apiEndpoints | $this->pagebuilder_widgets as a JSON array |
| Frontend | Optional (admin layout/index.vue) | Required (template/index.vue) |
| Stored in | plugin_backend table | page_widgets table |
A plugin can ship both at the same time — e.g. an admin area for managing testimonials plus a widget that renders them on the frontend. See Plugin Anatomy and the example below.
Why build widgets?
| Use case | Example |
|---|---|
| Recurring content block | Title banner, USP icon list, testimonial |
| Integration with a data source | Blog preview, product teaser, Instagram feed |
| Visual element without a DB | Spacer, button, image overlay text |
| Full sub-page | Shop checkout, user center, 3D exhibition |
| Forms | Newsletter signup, custom form |
The CMS already ships ~42 widgets (see Widget catalog below). Your own widgets plug into the same picker seamlessly.
Widget lifecycle
1. install() # Plugin sets $this->pagebuilder_widgets = [JSON...]
→ Entry in page_widgets (widget registry) per widget type
2. vueRegistry rebuild # /admin/cache or cache rebuild
→ Scans every widgets/*/template/index.vue
→ Generates plugin-registry.js per theme
3. Picker in the Pagebuilder # WidgetPickerModal.vue
→ Reads page_widgets, shows widget_title + widget_icon + widget_subtitle
4. Editor drops the widget onto a page
→ Row in page_row_col_widget (DB)
5. Modal form renders widget_menu fields
→ Admin user fills in fields → PATCH /api/backend/pagebuilder
6. Frontend rendering (PageService)
→ Widget array is passed to PagebuilderWidget.vue
→ getWidgetContent() loads dynamic data if needed (PHP)
→ plugin-registry.js resolves widget_template → Vue component
→ Component receives { data, content } as its widget propRegistration in the plugin's bootstrap.php
The actual widget registration happens inside the plugin — not in the widget folder's own bootstrap.php. Example from widgetsBase:
// widgetsBase/bootstrap.php (excerpt from install())
$this->pagebuilder_widgets = [
'{
"widget_title": "Title",
"widget_icon": "icon.png",
"widget_subtitle": "Insert a title (H1-H6)",
"widget_template": "title",
"widget_menu": [
{"row": "title", "type": "text", "placeholder": "Title"},
{"row": "subtitle", "type": "select", "placeholder": "Type",
"value": "id", "title": "title",
"options": [
{"id": "0", "title": "H1"},
{"id": "1", "title": "H2"},
{"id": "2", "title": "H3"}
]}
]
}',
'{
"widget_title": "Text / HTML",
"widget_icon": "icon.png",
"widget_template": "html",
"widget_menu": [
{"row": "text", "type": "textarea", "placeholder": "Text / HTML"}
]
}'
];Each entry is a JSON string with at least widget_title, widget_icon, widget_template, and widget_menu. For the field-type schema, see Widget menu field types.
After changes to the array: uninstall/reinstall the plugin or update page_widgets manually. After changes to template/index.vue: trigger a vueRegistry rebuild under /admin/cache.
Database tables
| Table | Purpose |
|---|---|
page_widgets | Widget registry (one entry per widget type, from $this->pagebuilder_widgets) |
page_row_col_widget | Placed widget instances per page (with columns like title, subtitle, text, image, …) |
page_widget_item | Items for repeater widgets (sub-records with base_id/language_short) |
Columns in page_row_col_widget map directly to the allowed row values in widget_menu. Details: Widget menu field types › DB columns.
Widget catalog
Built-in widgets by plugin:
| Plugin | Widgets |
|---|---|
widgetsBase | title, html, image, footer_links, social_footer, pagebuilder_element |
spacer / button / imageButton / imageOverlayText | one widget each |
titleBanner / titleBannerElement | banner elements |
infoBoxes / uspIconList / instagramJourney / locationMap / jobs / gallery | content lists with items |
blog | blog, blogpreview |
emailmarketing | newsletter |
productListing | productListing |
shop | productTeaser, categoryBannerNoLink, newReleasesNoLink, user, shop, checkout, abobutton, calendar |
shopware6 | sw6ProductListing, sw6ProductDetail, sw6Cart, sw6Checkout, sw6UserAccount, sw6CategoryTeaser, sw6SearchBar |
elasticsearch | elasticsearch, searchbar |
elearning | elearning |
exhibition | exhibition |
pagebuilder | form |
About ~42 widgets in total. The full field-definition reference lives in the pagebuilder-widgets skill (references/widget-catalog.md in the repo).
shop/widgets/email is not a Pagebuilder widget
The folder _public/extensions/core/backend/shop/widgets/email/ lives under widgets/ for legacy reasons, but it's not a picker widget — it's a PHPMailer-based template for order confirmation emails. Similarly, menueditor registers no Pagebuilder widgets (a pure admin plugin with content_construct=template).
Minimal widget
The fastest path to a working widget:
_public/extensions/core/backend/{plugin}/
└── widgets/
└── helloWidget/
├── bootstrap.php
├── icon.png
└── template/
└── index.vue// bootstrap.php
<?php
class helloWidget_PageBuilderPlugin
{
public static function getVersion() { return "1.0.0"; }
public static function getName() { return "Hello Widget"; }
public static function getWidgetContent($widget_array, $backend = false)
{
return null; // no dynamic data
}
}<!-- template/index.vue -->
<template>
<div class="hello-widget">
<h2>{{ widget.data?.title || 'Hello!' }}</h2>
</div>
</template>
<script setup lang="ts">
defineProps<{ widget: { data?: Record<string, any>; content?: any } }>()
</script>Registration lives in the plugin's bootstrap.php (not in this widget folder). For a full copy-paste example, see Testimonial Widget.
See also
- Widget Anatomy — directory structure +
_PageBuilderPluginclass in detail - Widget menu field types — all field types (text/textarea/select/filemanager/items)
- getWidgetContent() — loading dynamic data in PHP
- Widget items / repeater — sub-lists with
page_widget_item - Custom CSS convention —
.widget_{id}wrappers, PurgeCSS safelist - User-center items — plugin registration inside the user-center widget
- Example: Testimonial Widget — complete widget from scratch
- Plugin Anatomy — the surrounding plugin system