Twig Overrides
Guppy follows a component-based templating approach: dynamic, modular templates driven by theme.json and cleanly decoupled from Shopware defaults. The goal is update-friendliness without patching core code.
Why this approach?
- Maximum customisability: configuration values instead of code changes.
- Modularity: components (footer, header, widgets) as standalone templates.
- Upgrade compatibility: override via
sw_extends, no core touches. - Accessibility: modular templates allow targeted a11y improvements.
Theme integration via theme.json
Fields from theme.json are read in Twig via theme_config('field-name'). Example: footer variant.
{# src/Resources/views/storefront/base.html.twig #}
{% if theme_config('guppy-footer') != "default" %}
{% set configFooter = theme_config('guppy-footer') %}
{% endif %}
{% block base_footer %}
<footer class="footer-main{% if configFooter %} footer-main-{{ configFooter }} pt-md-3 pt-4{% endif %}">
{% block base_footer_inner %}
{% if configFooter %}
{% sw_include '@Storefront/storefront/layout/footer/footer-' ~ configFooter ~ '.html.twig' %}
{% else %}
{{ parent() }}
{% endif %}
{% endblock %}
</footer>
{% endblock %}Matching theme.json field:
{
"guppy-footer": {
"label": { "en-GB": "Footer variant", "de-DE": "Footer Variante" },
"type": "text",
"value": "guppy-default",
"custom": {
"componentName": "sw-single-select",
"options": [
{ "value": "default", "label": { "en-GB": "Shopware Default" } },
{ "value": "guppy-default", "label": { "en-GB": "Guppy Default" } }
]
},
"editable": true,
"tab": "footer",
"block": "layoutFooter",
"order": 100
}
}Variant template:
{# src/Resources/views/storefront/layout/footer/footer-guppy-default.html.twig #}
{% sw_extends '@Storefront/storefront/layout/footer/footer.html.twig' %}
{# Content here #}The variant system
Guppy applies the variant pattern consistently. Examples:
Header
{% if theme_config('guppy-header') != "default" %}
{% set configHeader = theme_config('guppy-header') %}
{% endif %}
{% block base_header %}
<header class="header-main{% if configHeader %} header-variant-{{ configHeader }}{% endif %}">
{% block base_header_inner %}
{% if configHeader %}
{% sw_include '@Storefront/storefront/layout/header/header-' ~ configHeader ~ '.html.twig' %}
{% else %}
{{ parent() }}
{% endif %}
{% endblock %}
</header>
{% endblock %}Available variants: default, compact (Guppy default), extended (with top bar), simple.
Navigation
{% if theme_config('guppy-navigation') != "content" %}
{% set navigationStyle = theme_config('guppy-navigation') %}
{% endif %}
{% block base_navigation %}
<nav class="nav-main{% if navigationStyle %} nav-{{ navigationStyle }}{% endif %}">
{% sw_include '@Storefront/storefront/layout/navbar/' ~ (navigationStyle ?: 'content') ~ '.html.twig' %}
</nav>
{% endblock %}Available variants: content, content-compact.
Product cards
{% if theme_config('guppy-productcard-config') %}
{% set boxDesign = theme_config('guppy-productcard-config') %}
{% endif %}
<article class="card product-box box-{{ layout }}{% if boxDesign %} box-{{ boxDesign }}{% endif %}">
<!-- Product card content -->
</article>Available variants: default, guppy-default (accessible). Additional fields:
"guppy-productcard-protective-frame": true,
"guppy-productcard-image-object-fit": "contain"Login & register / checkout
{% if theme_config('guppy-login-register') == "compact" %}
{% set loginStyle = "compact" %}
{% endif %}
{% if theme_config('guppy-checkout') == "compact" %}
{% set checkoutStyle = "compact" %}
{% endif %}Template structure
Selected paths under src/Resources/views/storefront/:
storefront/
├── base.html.twig # Skip links + layout
├── layout/
│ ├── header.html.twig
│ ├── header/
│ │ ├── header-compact.html.twig
│ │ ├── header-extended.html.twig
│ │ ├── header-simple.html.twig
│ │ ├── logo.html.twig
│ │ ├── search.html.twig
│ │ └── top-bar-extended.html.twig
│ ├── footer.html.twig
│ ├── footer/
│ │ └── footer-guppy-default.html.twig
│ ├── navbar/
│ │ ├── navbar.html.twig
│ │ ├── content.html.twig
│ │ └── content-compact.html.twig
│ ├── breadcrumb.html.twig
│ └── sidebar/
│ └── category-navigation-onelevel.html.twig
├── component/
│ ├── product/
│ ├── buy-widget/
│ ├── account/
│ ├── checkout/
│ ├── line-item/
│ ├── address/
│ ├── listing/
│ └── delivery-information.html.twig
├── element/ # CMS elements
├── page/
│ ├── account/
│ ├── checkout/
│ ├── content/
│ └── product-detail/
├── section/
│ └── cms-section-sidebar.html.twig
├── block/
└── utilities/
├── alert.html.twig
├── icon.html.twig
├── offcanvas.html.twig
└── thumbnail.html.twigFull list of overridden templates: Twig Blocks.
Custom Twig extensions
categoryOneLevel($navigationTree, $currentCategoryId)
Renders single-level category navigations:
{% set navigationData = categoryOneLevel(navigationTree, currentCategoryId) %}
{% for category in navigationData %}
<a href="{{ category.url }}" class="nav-link">
{{ category.name }}
</a>
{% endfor %}Implementation:
// src/Twig/CategoryOneLevelExtension.php
class CategoryOneLevelExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('categoryOneLevel', [$this, 'categoryOneLevel'])
];
}
public function categoryOneLevel($navigationTree, $currentCategoryId)
{
// single-level category navigation
}
}Accessibility
Skip links
Configurable via theme_config:
{# base.html.twig #}
{% block base_body_inner %}
{% if theme_config('guppy-header-skip-to-main-content') %}
<a href="#content-main" class="skip-link">
{{ 'skipLink.toMainContent'|trans|sw_sanitize }}
</a>
{% endif %}
{% if theme_config('guppy-header-skip-to-main-nav') %}
<a href="#main-navigation" class="skip-link">
{{ 'skipLink.toMainNavigation'|trans|sw_sanitize }}
</a>
{% endif %}
{% if theme_config('guppy-header-skip-to-search') %}
<a href="#search" class="skip-link">
{{ 'skipLink.toSearch'|trans|sw_sanitize }}
</a>
{% endif %}
{{ parent() }}
{% endblock %}Available skip-link fields:
| Field | Effect |
|---|---|
guppy-header-skip-to-main-content | Jump to main content |
guppy-header-skip-to-main-nav | Jump to main navigation |
guppy-header-skip-to-search | Jump to search |
USP banner
{# layout/_includes/usp-banner.html.twig #}
{% if theme_config('guppy-usp-active') %}
<div class="usp-banner" data-usp-banner-plugin="true">
{% if theme_config('guppy-usp-layout') == 'benefits' %}
{% for i in 1..4 %}
{% set benefit = theme_config('guppy-usp-layout-benefit' ~ i) %}
{% if benefit %}
<div class="usp-item">{{ benefit }}</div>
{% endif %}
{% endfor %}
{% else %}
<div class="usp-item">
<a href="{{ theme_config('guppy-usp-layout-link') }}"
{% if theme_config('guppy-usp-layout-newTab') %}target="_blank"{% endif %}>
{{ theme_config('guppy-usp-layout-text') }}
</a>
</div>
{% endif %}
</div>
{% endif %}Practical implementation
A new header type
- Create the template:
{# src/Resources/views/storefront/layout/header/header-custom.html.twig #}
{% sw_extends '@Storefront/storefront/layout/header/header.html.twig' %}
{% block layout_header_inner %}
<div class="header-custom">
{# Custom header content #}
</div>
{% endblock %}- Extend the theme configuration:
"guppy-header": {
"custom": {
"options": [
{ "value": "custom", "label": { "en-GB": "Custom Header" } }
]
}
}- Add SCSS:
.header-variant-custom {
.header-custom {
// Custom styles
}
}A new product card variant
{# src/Resources/views/storefront/component/product/card/box-custom.html.twig #}
{% sw_extends '@Storefront/storefront/component/product/card/box-standard.html.twig' %}
{% block component_product_box %}
<div class="product-box box-custom">
{# Custom content #}
</div>
{% endblock %}"guppy-productcard-config": {
"custom": {
"options": [
{ "value": "custom", "label": { "en-GB": "Custom product card" } }
]
}
}Alert variants
{# utilities/alert.html.twig #}
{% set alertClass = 'alert-' ~ type %}
{% if theme_config('guppy-alert-alert-outline-active') %}
{% set alertClass = alertClass ~ ' alert-outline' %}
{% endif %}
<div class="alert {{ alertClass }}" role="alert">
{{ message }}
</div>"guppy-alert-color-success": "#D3F2E7",
"guppy-alert-color-success-text": "dark",
"guppy-alert-alert-outline-active": trueBest practices
Inheritance done right
{# Always extend the matching Shopware template #}
{% sw_extends '@Storefront/storefront/layout/header/header.html.twig' %}
{# Override only the specific blocks #}
{% block layout_header_logo %}
<div class="header-logo-custom">
{{ parent() }}
</div>
{% endblock %}Consume theme config
{# Capture variant #}
{% if theme_config('guppy-component-variant') %}
{% set variant = theme_config('guppy-component-variant') %}
{% endif %}
{# Fallback to default #}
{% if variant %}
{# Custom logic #}
{% else %}
{{ parent() }}
{% endif %}Semantics and a11y
<nav role="navigation" aria-label="{{ 'navigation.main'|trans }}">
{# Navigation content #}
</nav>
<a href="#main-content" class="skip-link">
{{ 'skipLink.toMainContent'|trans }}
</a>Related
- Architecture: plugin structure, theme.json model.
- Variables & Tokens: SCSS counterparts to Twig configuration.
- Storefront JS: JS plugins that interact with templates.
- Twig Blocks: full override list.