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