Skip to content

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 ​

AspectAdmin pluginPagebuilder widget
WhatModule in the backend menuBuilding 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
FrontendOptional (admin layout/index.vue)Required (template/index.vue)
Stored inplugin_backend tablepage_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 caseExample
Recurring content blockTitle banner, USP icon list, testimonial
Integration with a data sourceBlog preview, product teaser, Instagram feed
Visual element without a DBSpacer, button, image overlay text
Full sub-pageShop checkout, user center, 3D exhibition
FormsNewsletter 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 prop

Registration 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:

php
// 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 ​

TablePurpose
page_widgetsWidget registry (one entry per widget type, from $this->pagebuilder_widgets)
page_row_col_widgetPlaced widget instances per page (with columns like title, subtitle, text, image, …)
page_widget_itemItems 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:

PluginWidgets
widgetsBasetitle, html, image, footer_links, social_footer, pagebuilder_element
spacer / button / imageButton / imageOverlayTextone widget each
titleBanner / titleBannerElementbanner elements
infoBoxes / uspIconList / instagramJourney / locationMap / jobs / gallerycontent lists with items
blogblog, blogpreview
emailmarketingnewsletter
productListingproductListing
shopproductTeaser, categoryBannerNoLink, newReleasesNoLink, user, shop, checkout, abobutton, calendar
shopware6sw6ProductListing, sw6ProductDetail, sw6Cart, sw6Checkout, sw6UserAccount, sw6CategoryTeaser, sw6SearchBar
elasticsearchelasticsearch, searchbar
elearningelearning
exhibitionexhibition
pagebuilderform

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
php
// 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
    }
}
vue
<!-- 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 ​