Verified on Shopware 6.7
What We're Building
We'll create a custom CMS block called "Feature Highlight" - a commonly requested component that displays a title, description, image, and a call-to-action button. The kind of block that marketing teams ask for constantly.
By the end, you'll understand the full lifecycle: registration, preview, config, storefront rendering.
File Structure
YourPlugin/
└── src/
└── Resources/
├── app/
│ └── administration/
│ └── src/
│ └── module/
│ └── sw-cms/
│ ├── blocks/
│ │ └── feature-highlight/
│ │ ├── index.js
│ │ ├── component/
│ │ │ ├── index.js
│ │ │ └── sw-cms-block-feature-highlight.html.twig
│ │ └── preview/
│ │ ├── index.js
│ │ └── sw-cms-preview-feature-highlight.html.twig
│ └── elements/
│ └── feature-highlight/
│ ├── index.js
│ ├── component/
│ │ ├── index.js
│ │ └── sw-cms-el-feature-highlight.html.twig
│ └── config/
│ ├── index.js
│ └── sw-cms-el-config-feature-highlight.html.twig
└── views/
└── storefront/
└── element/
└── cms-element-feature-highlight.html.twig
Step 1: Register the CMS Element
The element defines what data the block holds.
// elements/feature-highlight/index.js
import './component';
import './config';
Shopware.Service('cmsService').registerCmsElement({
name: 'feature-highlight',
label: 'Feature Highlight',
component: 'sw-cms-el-feature-highlight',
configComponent: 'sw-cms-el-config-feature-highlight',
defaultConfig: {
title: {
source: 'static',
value: 'Your Feature Title',
},
description: {
source: 'static',
value: 'Describe your feature here.',
},
buttonLabel: {
source: 'static',
value: 'Learn More',
},
buttonUrl: {
source: 'static',
value: '#',
},
media: {
source: 'static',
value: null,
entity: {
name: 'media',
},
},
},
});
Step 2: Element Component (Admin Preview)
What editors see in the CMS editor:
// elements/feature-highlight/component/index.js
import template from './sw-cms-el-feature-highlight.html.twig';
Shopware.Component.register('sw-cms-el-feature-highlight', {
template,
mixins: [
Shopware.Mixin.getByName('cms-element'),
],
computed: {
title() {
return this.element.config.title.value;
},
description() {
return this.element.config.description.value;
},
buttonLabel() {
return this.element.config.buttonLabel.value;
},
mediaUrl() {
const media = this.element.data?.media;
return media?.url || '';
},
},
created() {
this.initElementConfig('feature-highlight');
this.initElementData('feature-highlight');
},
});
<!- sw-cms-el-feature-highlight.html.twig ->
<div class="sw-cms-el-feature-highlight">
<div class="sw-cms-el-feature-highlight__image" v-if="mediaUrl">
<img :src="mediaUrl" alt="">
</div>
<div class="sw-cms-el-feature-highlight__content">
<h2>{{ title }}</h2>
<p>{{ description }}</p>
<button class="sw-button sw-button-primary">{{ buttonLabel }}</button>
</div>
</div>
Step 3: Element Config (Sidebar Form)
The configuration sidebar where editors fill in values:
// elements/feature-highlight/config/index.js
import template from './sw-cms-el-config-feature-highlight.html.twig';
Shopware.Component.register('sw-cms-el-config-feature-highlight', {
template,
mixins: [
Shopware.Mixin.getByName('cms-element'),
],
created() {
this.initElementConfig('feature-highlight');
},
});
<!- sw-cms-el-config-feature-highlight.html.twig ->
<div class="sw-cms-el-config-feature-highlight">
<sw-text-field
label="Title"
v-model="element.config.title.value">
</sw-text-field>
<sw-textarea-field
label="Description"
v-model="element.config.description.value">
</sw-textarea-field>
<sw-text-field
label="Button Label"
v-model="element.config.buttonLabel.value">
</sw-text-field>
<sw-text-field
label="Button URL"
v-model="element.config.buttonUrl.value">
</sw-text-field>
<sw-media-field
label="Image"
v-model="element.config.media.value">
</sw-media-field>
</div>
Step 4: Register the Block
The block wraps the element and appears in the CMS block picker:
// blocks/feature-highlight/index.js
import './component';
import './preview';
Shopware.Service('cmsService').registerCmsBlock({
name: 'feature-highlight',
label: 'Feature Highlight',
category: 'text-image',
component: 'sw-cms-block-feature-highlight',
previewComponent: 'sw-cms-preview-feature-highlight',
defaultConfig: {
marginBottom: '20px',
marginTop: '20px',
marginLeft: '20px',
marginRight: '20px',
sizingMode: 'boxed',
},
slots: {
content: 'feature-highlight',
},
});
// blocks/feature-highlight/component/index.js
import template from './sw-cms-block-feature-highlight.html.twig';
Shopware.Component.register('sw-cms-block-feature-highlight', {
template,
});
<!- sw-cms-block-feature-highlight.html.twig ->
<div class="sw-cms-block-feature-highlight">
<slot name="content">
<sw-cms-slot name="content"></sw-cms-slot>
</slot>
</div>
Step 5: Block Preview (Thumbnail in Picker)
// blocks/feature-highlight/preview/index.js
import template from './sw-cms-preview-feature-highlight.html.twig';
Shopware.Component.register('sw-cms-preview-feature-highlight', {
template,
});
<!- sw-cms-preview-feature-highlight.html.twig ->
<div class="sw-cms-preview-feature-highlight">
<div style="display:flex;gap:12px;padding:12px;background:#f9f9f9;border-radius:4px;">
<div style="width:40px;height:40px;background:#ddd;border-radius:4px;"></div>
<div style="flex:1;">
<div style="height:8px;background:#ccc;width:60%;margin-bottom:6px;border-radius:2px;"></div>
<div style="height:6px;background:#eee;width:80%;border-radius:2px;"></div>
</div>
</div>
</div>
Step 6: Storefront Rendering
Finally, how it renders on the actual storefront:
{# views/storefront/element/cms-element-feature-highlight.html.twig #}
{% block element_feature_highlight %}
<div class="cms-element-feature-highlight">
{% if element.data.media %}
<div class="feature-highlight__image">
{% sw_thumbnails 'feature-highlight-image' with {
media: element.data.media,
sizes: { 'default': '400px' }
} %}
</div>
{% endif %}
<div class="feature-highlight__content">
<h2 class="feature-highlight__title">
{{ element.config.title.value }}
</h2>
<p class="feature-highlight__description">
{{ element.config.description.value }}
</p>
{% if element.config.buttonUrl.value %}
<a href="{{ element.config.buttonUrl.value }}"
class="btn btn-primary feature-highlight__button">
{{ element.config.buttonLabel.value }}
</a>
{% endif %}
</div>
</div>
{% endblock %}
Step 7: Register Everything in main.js
// administration/src/main.js
import './module/sw-cms/blocks/feature-highlight';
import './module/sw-cms/elements/feature-highlight';
Build and Test
# Build admin
bin/build-administration.sh
# Clear cache
bin/console cache:clear
# Open admin → Content → Shopping Experiences
# Your "Feature Highlight" block should appear under "Text & Images"
Tips
- Always provide sensible defaults - editors shouldn't see blank blocks
- Use
sw_thumbnailsin storefront - never hardcode image URLs - Category matters - put blocks in the right category so editors can find them
- Test with empty data - what happens when an editor leaves fields blank?
Need custom CMS blocks for your Shopware shop? We build them.