What the law requires from 19 June 2026
Directive (EU) 2023/2673, which adds a withdrawal function to the Consumer Rights Directive, requires every B2C shop in the EU to provide a two-step withdrawal button. Step 1 reads “Withdraw from contract”. Step 2 reads “Confirm withdrawal”. In between sits a form with name plus a means of contact as mandatory fields, followed by an acknowledgement of receipt by email on a durable medium.
No login, no scroll barrier. Available throughout the entire withdrawal period. The risk of a warning letter for breaches typically ranges from EUR 500 to 2,000, plus an automatic extension of the withdrawal period to twelve months and fourteen days.
The two paths for Shopware 6
Path A: a Twig template extension directly in your theme. Fast, but tied to your theme. You have to be careful during theme updates.
Path B: your own storefront plugin that registers a Twig snippet. Clean, update-safe, and can be deactivated from the admin. This is the recommended Shopware path.
Path A: Twig template extension in the theme
Shopware 6 uses block-based Twig templates. You can override or extend any block. The footer lives in @Storefront/storefront/layout/footer/footer.html.twig. We hook into the block base_footer_inner_container.
{# Resources/views/storefront/layout/footer/footer.html.twig #}
{% sw_extends '@Storefront/storefront/layout/footer/footer.html.twig' %}
{% block base_footer_inner_container %}
{{ parent() }}
<script
src="https://widerrufbutton.net/widget/v1/wh.js"
data-shop-id="{{ config('WiderrufButton.config.shopId') }}"
data-position="footer"
data-lang="de"
async
defer
></script>
{% endblock %}The config(...) call accesses the plugin configuration. If you do not want to build a configuration, you can also write the widget key directly into the snippet. For a clean solution you need Path B, though.
Path B: your own plugin
A Shopware 6 plugin is essentially a Symfony bundle with a composer.json, a plugin class and a services.xml for dependency injection.
The minimal structure looks like this:
custom/plugins/WiderrufButton/
├── composer.json
├── src/
│ ├── WiderrufButton.php
│ ├── Resources/
│ │ ├── config/
│ │ │ ├── config.xml
│ │ │ └── services.xml
│ │ └── views/
│ │ └── storefront/
│ │ └── layout/
│ │ └── footer/
│ │ └── footer.html.twigThe config.xml defines the input field for the widget key in the Shopware admin:
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/platform/trunk/src/Core/System/SystemConfig/Schema/config.xsd">
<card>
<title>WiderrufButton</title>
<input-field>
<name>shopId</name>
<label>Widget-Key</label>
<helpText>Aus dem WiderrufButton-Dashboard</helpText>
</input-field>
</card>
</config>The services.xml stays empty as long as you do not need your own services. The Twig template overrides the footer block just like in Path A, but calls the plugin configuration via config('WiderrufButton.config.shopId').
Activate and test the plugin
From the Shopware directory, run the commands in this order:
bin/console plugin:refresh
bin/console plugin:install --activate WiderrufButton
bin/console theme:compile
bin/console cache:clearThe plugin then appears in the admin under Extensions. Enter the widget key from your WiderrufButton dashboard there. Save. Open the shop and check the footer.
Mind the HTTP cache and CSP
Shopware 6 caches aggressively. After compiling the theme, clear the HTTP cache, otherwise you will not see the script right away. In the Shopware configuration that is bin/console http:cache:clear.
If you have a Content Security Policy active, widerrufbutton.net must appear in the script-src and connect-src directives. The browser loads the script from there and sends the withdrawal to the API route on the same domain.
Why the Shadow DOM matters
Shopware themes ship their own CSS. Without encapsulation the button styling would collide with the theme, the colours would render wrong, the modal would appear in the wrong place. That is why the widget runs inside the Shadow DOM. Its CSS is fully isolated, Shopware styles cannot override anything, and your theme stays untouched.
The same is true in the other direction: the button cannot do anything to your theme. No global styles, no class names that collide, no fonts that get overridden unintentionally.
Multilingual and sales channels
Shopware allows multiple sales channels with different domains. For each domain you need a separate shop entry in the WiderrufButton dashboard, so the CORS check runs against the correct origin. The widget keys differ, and you have to bind the right key to the plugin configuration per sales channel.
You will find the details on the product page and in the tutorial.
Checklist before go-live
- Widget key entered and saved in the admin
- Theme recompiled, HTTP cache cleared
- Button visible in the footer, reachable without scrolling
- Form submitted with test data, email arrives within seconds
- Content Security Policy allows the widget domain
- The test entry appears in the dashboard under Withdrawals