Accessibility fixes in your Shopware theme’s code
Validiscan fixes accessibility failures in your Shopware 6 theme plugin’s Twig templates, SCSS, storefront JavaScript and snippet files. Nothing reaches your live shop until your developer merges or applies the changes and deploys the theme.
Last updated:
On this page: 6 answers
What does Validiscan change in a Shopware theme?
Validiscan changes only files in the folders where a Shopware 6 storefront theme keeps its sources, Resources/views, Resources/ and Resources/snippet, and its theme.json. A change to any other file fails validation and is never delivered, and no fix installs a plugin or adds an overlay script.
Settings that you keep in the Shopware Administration, such as meta titles or theme colours saved there, are not files of the theme, so the fixes do not change them. A project covers one domain and one theme; third-party embeds such as payment or chat widgets are not included in it.
Details: the folders, what a change looks like, and the two recipes that change the storefront script
Validiscan changes only files in the folders where a Shopware 6 storefront theme keeps its sources: the Twig templates in Resources/views, the SCSS and JavaScript sources in Resources/, the snippet files with the storefront’s text in Resources/snippet, and the theme’s theme.json. A change to any other file, such as the compiled files in Resources/ or the plugin’s PHP code and configuration, fails validation. A change that fails validation cannot be approved, so it is never delivered.
Every change is an ordinary edit to a theme file that your developer can read line by line. Small changes, such as an added attribute or a corrected colour value, are made where the code already is. New style rules and new blocks of script sit between comments that name the recipe, for example /* validiscan:, so they are easy to find. Two recipes change the theme’s own storefront script: one connects a pause button to the image slider, and the other keeps a script’s autoplay from starting when a visitor has asked for less motion. No fix installs a plugin or adds a script that corrects your pages after they load, as an overlay does.
How does Validiscan get access to your Shopware theme?
Through a GitHub repository, or a ZIP of the theme plugin uploaded in the customer portal, because Validiscan prepares every fix on a copy of your Shopware theme plugin’s source code. Which one fits depends on where the code lives: a repository gets a pull request, a ZIP gets a patch bundle.
| Way in | You give | Changes come back as |
|---|---|---|
| Through your GitHub repository | Access to the repository that holds the theme plugin, through Validiscan’s GitHub App or an access token | A pull request |
| From a ZIP of the theme plugin | The theme plugin’s folder under custom/, as one ZIP of up to 100 MB, uploaded in the customer portal by an admin of your organization | A patch bundle |
Through your GitHub repository
If the code of your theme plugin is kept in a GitHub repository, you give Validiscan access to that repository: you install Validiscan’s GitHub App on it, or give Validiscan an access token for it. Validiscan’s setup specifies the App’s permissions as read and write access to the repository’s contents and pull requests and read access to its metadata, and nothing else. A token needs the same permissions on that one repository.
The theme plugin can sit at the top of the repository or deeper in it, as in custom/: the recipes find the theme’s files by Shopware’s folder conventions. The changes then arrive as a pull request.
From a ZIP of the theme plugin
If the code is not in a GitHub repository, your developer makes a ZIP of the theme plugin. The customer portal’s instruction for Shopware reads: “Ask your developer for a ZIP of your theme plugin: its folder under custom/plugins, as it is deployed to your shop.” An admin of your organization then uploads the file in the customer portal, as one ZIP file of up to 100 MB. Validiscan prepares the changes on that copy of your theme and sends them back as a patch bundle.
Without either of the two, Validiscan has no code to change, and no fix is prepared.
How do the changes reach your shop?
The changes arrive as a pull request or as a patch bundle, and your developer decides when they go live. Validiscan delivers only changes it has reviewed and approved and never changes your live Shopware shop’s theme or code; delivery as an unpublished theme copy exists only on Shopify.
- Validiscan delivers the changes it has reviewed and approved: a pull request on a new branch, or a patch bundle for your developer.
- Your developer reviews the changes. For a pull request, its checklist asks for a preview or staging environment, not the live shop, and a check of the affected pages, also with the keyboard alone.
- Your developer merges the pull request or applies the patch bundle.
- Your developer builds the theme: SCSS changes take effect once the theme is compiled again, and JavaScript changes once the storefront is built again.
- Your developer deploys the theme. Once the changes are live, a verification scan checks the affected pages again.
A pull request
With access to your repository, Validiscan creates a new branch named validiscan/, commits each fix on its own and opens one pull request with a summary table, an explanation of each change and a review checklist. Nothing is pushed to your default branch. The checklist asks your developer to deploy the branch to a preview or staging environment, not to the live shop, and to check the affected pages there, also with the keyboard alone, before merging.
A patch bundle
From the uploaded ZIP, Validiscan prepares a zip archive and sends it to your developer. It contains PATCH.md with every change and how to apply it, all changes in one patch file that applies with git apply, one patch file per fix, the complete changed files and a README.md. Your developer applies the changes, tests them and deploys the theme. What a pull request and a patch bundle contain
Building the theme
A Shopware shop does not serve the theme’s SCSS and JavaScript sources as they are: they are built first. Changes to the SCSS take effect once the theme is compiled again, for example with bin/, as Shopware’s developer documentation on theme styling describes. Changes to the storefront JavaScript take effect once the storefront is built again, as described in Shopware’s documentation on storefront JavaScript. Your developer runs these steps when deploying the changed theme. How accessibility fixes reach a Shopware theme, and what a theme update can undo
What do the recipes change in a Shopware theme?
Each of Validiscan’s 20 recipes for Shopware themes is a deterministic change for one kind of failure: the same code always gets the same change. A recipe makes a change only where the theme’s code shows what the right change is; where it does not, it changes nothing.
Page structure and navigation
-
Page language in the base layout html-lang The
<html>element of the base layout gets a validlangattribute where it is missing or where the theme prints a locale code such asen_GB.The
<html>element of the storefront’s base layout gets a validlangattribute, so screen readers pronounce the page in the right language. Where the attribute is missing, the recipe adds the code of the shop’s active language, for examplede-DE, the value Shopware’s own base template prints. Where the theme prints a locale code such asen_GB, which is not a valid language tag, the template now turns it intoen-GB. A page whose content is written in another language than the shop’s, such as a German legal text in an English shop, is left to you: the language of that content is set in the Administration or marked in the text itself.- Rules it serves
-
-
html-has-langAUTO checked through vs-lang -
html-lang-validAUTO checked through vs-lang -
vs-langAUTO
-
- File types it changes
-
.html.twig
normalises the locale code of the base layout; German content pages are left to the merchant Example data Resources/views/storefront/base.html.twig Changes to Resources/views/storefront/base.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/base.html.twig: 1 added, 1 removed Line before Line after Change Code 9 9 {% block base_doctype %}<!DOCTYPE html>{% endblock %} 10 10 11 11 {% block base_html %} 12 Removed <html lang="{{ page.lang }}" itemscope="itemscope" itemtype="https://schema.org/WebPage"> 12 Added <html lang="{{ page.lang|replace({'_': '-'}) }}" itemscope="itemscope" itemtype="https://schema.org/WebPage"> 13 13 {% endblock %} 14 14 15 15 {% block base_head %} -
Skip link, main area and named menus skip-link-main Depending on what the scan found: a skip link to the main content, a marked main area, or names for the navigation areas.
Depending on what the scan found, the recipe adds a skip link, marks the main content or names the navigation areas. The skip link becomes the first element a keyboard user reaches on each page. It stays hidden until it receives focus and jumps to the main content. Where no template marks the main content as
<main>, the content area of the base layout becomes the page’s<main>area. The link’s text is the snippetgeneral.skipToContent, in English “Skip to main content”. An existing skip link is pointed at the main content. Navigation areas without a name get one, taken from their heading or from their purpose, such as Main navigation, Service navigation or Breadcrumb. A navigation area whose purpose is unclear, or that would share its name with another one, is left to a person.- Rules it serves
-
-
bypassAUTO checked through vs-headings-landmarks -
landmark-one-mainAUTO checked through vs-headings-landmarks -
regionAUTO -
skip-linkAUTO -
vs-headings-landmarksAUTO
-
- File types it changes
-
.html.twig.json.scss
adds a skip link, a <main> landmark and navigation names; the landmark problems are gone Example data Resources/app/storefront/src/scss/base.scss Changes to Resources/app/storefront/src/scss/base.scss: 24 added, 0 removed
Changes to Resources/app/storefront/src/scss/base.scss: 24 added, 0 removed Line before Line after Change Code 1708 1708 margin: 8px 0 0; 1709 1709 font-size: 0.875rem; 1710 1710 } 1711 Added 1712 Added /* validiscan:skip-link-main */ 1713 Added /* Skip link: parked off-screen, shown at the top left when it receives keyboard focus (WCAG 2.4.1). */ 1714 Added .skip-to-content-link { 1715 Added position: absolute; 1716 Added top: 8px; 1717 Added left: -10000px; 1718 Added z-index: 10000; 1719 Added padding: 12px 16px; 1720 Added border: 2px solid #1f2328; 1721 Added background: #ffffff; 1722 Added color: #1f2328; 1723 Added font-weight: 700; 1724 Added } 1725 Added 1726 Added .skip-to-content-link:focus { 1727 Added left: 8px; 1728 Added } 1729 Added 1730 Added /* The skip target takes focus programmatically; it needs no focus ring of its own. */ 1731 Added #content-main:focus { 1732 Added outline: none; 1733 Added } 1734 Added /* /validiscan:skip-link-main */ Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 7 added, 0 removed
Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 7 added, 0 removed Line before Line after Change Code 193 193 "notFoundText": "Die gesuchte Seite existiert nicht oder wurde verschoben.", 194 194 "backToHome": "Zurück zur Startseite", 195 195 "searchInstead": "Unsere Kaffees durchsuchen" 196 Added }, 197 Added "a11y": { 198 Added "navigation": { 199 Added "breadcrumb": "Brotkrümelnavigation", 200 Added "service": "Servicenavigation", 201 Added "main": "Hauptnavigation" 202 Added } 196 203 } 197 204 } Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 7 added, 0 removed
Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 7 added, 0 removed Line before Line after Change Code 193 193 "notFoundText": "The page you are looking for does not exist or has moved.", 194 194 "backToHome": "Back to the homepage", 195 195 "searchInstead": "Search our coffees" 196 Added }, 197 Added "a11y": { 198 Added "navigation": { 199 Added "breadcrumb": "Breadcrumb", 200 Added "service": "Service navigation", 201 Added "main": "Main navigation" 202 Added } 196 203 } 197 204 } Resources/views/storefront/base.html.twig Changes to Resources/views/storefront/base.html.twig: 4 added, 3 removed
Changes to Resources/views/storefront/base.html.twig: 4 added, 3 removed Line before Line after Change Code 22 22 <body class="{% block base_body_classes %}is-ctl-{{ page.controllerName }} is-act-{{ page.controllerAction }} sw-storefront sw-theme-kaffeekontor-nord{% endblock %}"> 23 23 24 24 {% block base_body_skip_to_content %} 25 Removed {% endblock %} 25 Added <a class="skip-to-content-link" href="#content-main">{{ 'general.skipToContent'|trans|sw_sanitize }}</a> 26 Added {% endblock %} 26 27 27 28 {% block base_body_inner %} 28 29 {% block base_noscript %} 48 49 {% endblock %} 49 50 50 51 {% block base_main %} 51 Removed <div class="content-main"> 52 Added <main class="content-main" id="content-main" tabindex="-1"> 52 53 {% block base_main_inner %} 53 54 <div class="container"> 54 55 {% block base_main_container %} 64 65 {% endblock %} 65 66 </div> 66 67 {% endblock %} 67 Removed </div> 68 Added </main> 68 69 {% endblock %} 69 70 70 71 {% block base_footer %} Resources/views/storefront/layout/breadcrumb.html.twig Changes to Resources/views/storefront/layout/breadcrumb.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/layout/breadcrumb.html.twig: 1 added, 1 removed Line before Line after Change Code 1 1 {% block layout_breadcrumb %} 2 Removed <nav class="breadcrumb-nav"> 2 Added <nav class="breadcrumb-nav" aria-label="{{ 'a11y.navigation.breadcrumb'|trans|striptags }}"> 3 3 <ol class="breadcrumb" itemscope itemtype="https://schema.org/BreadcrumbList"> 4 4 {% for item in items %} 5 5 <li class="breadcrumb-item{% if loop.last %} active{% endif %}" Resources/views/storefront/layout/footer/footer.html.twig Changes to Resources/views/storefront/layout/footer/footer.html.twig: 2 added, 2 removed
Changes to Resources/views/storefront/layout/footer/footer.html.twig: 2 added, 2 removed Line before Line after Change Code 23 23 {% block layout_footer_navigation_service %} 24 24 <div class="col-md-6 col-lg-3 footer-column"> 25 25 <div class="footer-column-headline" id="footer-headline-service">{{ 'footer.serviceMenuHeadline'|trans }}</div> 26 Removed <nav class="footer-column-content"> 26 Added <nav class="footer-column-content" aria-labelledby="footer-headline-service"> 27 27 <ul class="list-unstyled footer-service-menu-list"> 28 28 <li class="footer-service-menu-item"><a class="footer-link" href="{{ url('/contact') }}">{{ 'footer.linkContact'|trans }}</a></li> 29 29 <li class="footer-service-menu-item"><a class="footer-link" href="{{ url('/brewing-guide') }}">{{ 'footer.linkBrewingGuide'|trans }}</a></li> 38 38 {% block layout_footer_navigation_information %} 39 39 <div class="col-md-6 col-lg-3 footer-column"> 40 40 <div class="footer-column-headline" id="footer-headline-information">{{ 'footer.informationHeadline'|trans }}</div> 41 Removed <nav class="footer-column-content"> 41 Added <nav class="footer-column-content" aria-labelledby="footer-headline-information"> 42 42 <ul class="list-unstyled footer-service-menu-list"> 43 43 {% for category in categories %} 44 44 <li class="footer-service-menu-item"><a class="footer-link" href="{{ category.url }}">{{ category.name }}</a></li> Resources/views/storefront/layout/header/top-bar.html.twig Changes to Resources/views/storefront/layout/header/top-bar.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/layout/header/top-bar.html.twig: 1 added, 1 removed Line before Line after Change Code 1 1 {% block layout_header_top_bar %} 2 2 <div class="top-bar"> 3 Removed <nav class="top-bar-nav"> 3 Added <nav class="top-bar-nav" aria-label="{{ 'a11y.navigation.service'|trans|striptags }}"> 4 4 {% block layout_header_top_bar_usp %} 5 5 <p class="top-bar-usp"> 6 6 {% sw_icon 'checkmark' style { size: 'xs' } %} Resources/views/storefront/layout/navigation/navigation.html.twig Changes to Resources/views/storefront/layout/navigation/navigation.html.twig: 2 added, 1 removed
Changes to Resources/views/storefront/layout/navigation/navigation.html.twig: 2 added, 1 removed Line before Line after Change Code 3 3 <div class="container"> 4 4 {% block layout_main_navigation_menu %} 5 5 <nav class="nav main-navigation-menu" itemscope="itemscope" 6 Removed itemtype="https://schema.org/SiteNavigationElement"> 6 Added itemtype="https://schema.org/SiteNavigationElement" 7 Added aria-label="{{ 'a11y.navigation.main'|trans|striptags }}"> 7 8 {% block layout_main_navigation_menu_items %} 8 9 {% for item in navigation %} 9 10 {% set active = item.id == page.activeNavigationId %} -
A title on every page document-title-template Where the layout prints only the meta title, the shop name becomes its fallback, so no page has an empty title.
Where the theme’s layout writes only the page’s meta title into the
<title>, categories and CMS pages without a meta title get an empty title. The recipe keeps the theme’s own title and gives it a fallback: the shop name, taken from where the theme already prints it or, if it does not, from the shop name in Shopware’s basic settings. No page then has an empty title. A title that is only the shop name still does not say what the page is, so those pages need a meta title in the Administration. Titles that already have a fallback or fixed text are left alone.- Rules it serves
-
-
document-titleAUTO
-
- File types it changes
-
.html.twig
gives the layout title a shop-name fallback and the empty CMS title is gone Example data Resources/views/storefront/layout/meta.html.twig Changes to Resources/views/storefront/layout/meta.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/layout/meta.html.twig: 1 added, 1 removed Line before Line after Change Code 36 36 {% endblock %} 37 37 38 38 {% block layout_head_title %} 39 Removed <title>{{ page.metaTitle }}</title> 39 Added <title>{{ page.metaTitle|default(shop.name) }}</title> 40 40 {% endblock %} 41 41 42 42 {% block layout_head_stylesheet %} -
Headings without skipped levels heading-levels A heading that skips a level moves up, together with the template’s headings at its level or deeper, and keeps its look.
A heading that skips a level, such as an h3 right after an h1, moves up to the next level, and the other headings of the same template at that level or a deeper one move with it, so the template’s own outline stays intact. Where the template sets the heading’s tag in a variable, the variable is changed. Classes stay, and where no class sets the size, a Bootstrap heading class keeps the old look. A page without a main heading gets the element that already titles it in its own template turned into the h1, with its look kept. If a page has no such element, the recipe adds no heading, because a new heading needs wording.
- Rules it serves
-
-
heading-orderAUTO checked through vs-headings-landmarks -
page-has-heading-oneAUTO checked through vs-headings-landmarks -
vs-headings-landmarksAUTO
-
- File types it changes
-
.html.twig
moves a static grid title from h3 to h2; the skipped level is gone Example data Resources/views/storefront/block/cms-block-product-grid.html.twig Changes to Resources/views/storefront/block/cms-block-product-grid.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/block/cms-block-product-grid.html.twig: 1 added, 1 removed Line before Line after Change Code 5 5 {% block block_product_grid %} 6 6 <div class="cms-block cms-block-product-grid"> 7 7 <div class="cms-block-container"> 8 Removed <h3 class="cms-block-title">{{ block.title }}</h3> 8 Added <h2 class="cms-block-title">{{ block.title }}</h2> 9 9 <div class="row cms-listing-row"> 10 10 {% for product in block.products %} 11 11 {% sw_include '@Storefront/storefront/component/product/card/box-standard.html.twig' with { product: product } %}
Keyboard use and focus
-
A visible focus outline focus-visible-styles One outline for keyboard focus, in the theme’s brand colour or the nearest shade of its hue with a contrast of at least 3:1 against the page background.
The main stylesheet gets one outline for keyboard focus: 2 pixels, solid, 2 pixels away from the element, in the theme’s brand colour. Where that colour is too light, the nearest shade of the same hue that has a contrast of at least 3:1 against the page background is used. Theme rules that remove the outline on focus now apply only to focus by mouse: a click keeps the theme’s look, and keyboard focus shows the outline. Rules that show focus in another way are kept. On dark areas, such as a top bar, the outline may need a lighter colour, which your developer checks in the preview.
- Rules it serves
-
-
vs-focus-visibleAUTO
-
- File types it changes
-
.scss
adds a brand-coloured :focus-visible ring and scopes the outline resets Example data Resources/app/storefront/src/scss/_variant-broken.scss Changes to Resources/app/storefront/src/scss/_variant-broken.scss: 5 added, 5 removed
Changes to Resources/app/storefront/src/scss/_variant-broken.scss: 5 added, 5 removed Line before Line after Change Code 21 21 } 22 22 23 23 /* "Remove the ugly blue ring" (vs-focus-visible). */ 24 Removed .header-actions-btn:focus, 25 Removed .header-search-btn:focus, 26 Removed .main-navigation-link:focus, 27 Removed .product-name:focus, 28 Removed .footer-link:focus { 24 Added .header-actions-btn:focus:not(:focus-visible), 25 Added .header-search-btn:focus:not(:focus-visible), 26 Added .main-navigation-link:focus:not(:focus-visible), 27 Added .product-name:focus:not(:focus-visible), 28 Added .footer-link:focus:not(:focus-visible) { 29 29 outline: 0; 30 30 box-shadow: none; 31 31 } Resources/app/storefront/src/scss/base.scss Changes to Resources/app/storefront/src/scss/base.scss: 8 added, 0 removed
Changes to Resources/app/storefront/src/scss/base.scss: 8 added, 0 removed Line before Line after Change Code 1708 1708 margin: 8px 0 0; 1709 1709 font-size: 0.875rem; 1710 1710 } 1711 Added 1712 Added /* validiscan:focus-visible-styles */ 1713 Added /* Visible keyboard focus (WCAG 2.4.7): brand colour sw-color-brand-primary, 8.9:1 on #ffffff. */ 1714 Added :focus-visible { 1715 Added outline: 2px solid #6b3f1d; 1716 Added outline-offset: 2px; 1717 Added } 1718 Added /* /validiscan:focus-visible-styles */ -
A tab order that follows the page remove-positive-tabindex A
tabindexabove 0 is removed or set to 0, so the Tab key follows the order of the page, unless the template’s code writes it.A
tabindexabove 0 pulls an element to the front of the keyboard order. The recipe removes it from links, buttons, form fields and other elements that take focus anyway, and sets it to 0 on other elements, so they stay reachable while the Tab key follows the order of the page. Atabindexthat the template’s code writes is left to a person. A scan reports such a forced order twice: through the axe-core ruletabindex, whose findings get this change, and through Validiscan’s checkvs-focus-order, whose tier is MANUAL, so its findings come with guidance.- Rules it serves
-
-
tabindexAUTO -
vs-focus-orderMANUAL
-
- File types it changes
-
.html.twig
removes tabindex="1" from the buy button; vs-focus-order and axe tabindex stop firing Example data Resources/views/storefront/page/product-detail/buy-widget.html.twig Changes to Resources/views/storefront/page/product-detail/buy-widget.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/page/product-detail/buy-widget.html.twig: 1 added, 1 removed Line before Line after Change Code 82 82 </div> 83 83 <div class="buy-widget-submit"> 84 84 <button class="btn btn-primary btn-buy" 85 Removed title="{{ 'detail.addProduct'|trans }}" tabindex="1"> 85 Added title="{{ 'detail.addProduct'|trans }}"> 86 86 {{ 'detail.addProduct'|trans }} 87 87 </button> 88 88 </div>
Zoom and tap targets
-
Zoom allowed on phones meta-viewport-zoom The viewport tag loses
user-scalable=noand anymaximum-scalebelow 2, so visitors can enlarge text to at least 200 % on a phone.The recipe removes
user-scalable=noand anymaximum-scalebelow 2 from the viewport tag of the storefront templates, usually inlayout/meta.html.twig, so visitors can enlarge text to at least 200 % on a phone. For the best-practice rulemeta-viewport-largethe limit is 5. The other viewport settings stay as they were. If the lock was there to stop iOS from zooming into form fields, a font size of at least 16 pixels for the fields does that without blocking zoom.- Rules it serves
-
-
meta-viewportAUTO -
meta-viewport-largeAUTO
-
- File types it changes
-
.html.twig
removes the zoom lock from the fixture theme and the rule stops firing Example data Resources/views/storefront/layout/meta.html.twig Changes to Resources/views/storefront/layout/meta.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/layout/meta.html.twig: 1 added, 1 removed Line before Line after Change Code 6 6 7 7 {% block layout_head_meta_tags_viewport %} 8 8 {# Added by the merchant's agency to stop "zoom jumps" on iOS. #} 9 Removed <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> 9 Added <meta name="viewport" content="width=device-width, initial-scale=1"> 10 10 {% endblock %} 11 11 12 12 {% block layout_head_meta_tags_general %} -
Larger tap targets target-size-padding Buttons and links the scan found smaller than 24 by 24 pixels get that minimum size, where their class identifies them.
Buttons and links that the scan found smaller than 24 by 24 pixels, such as header icons, quantity buttons or swatches, get a minimum size of 24 by 24 pixels. The minimum is added to the theme’s own style rule that made the control small, next to the declaration that shrank it, so the order of the theme’s styles stays the same and the icons inside keep their size. A control that is small without such a rule gets a minimum size for its class in the main stylesheet. A control the recipe cannot identify by its class is left to a person.
- Rules it serves
-
-
vs-target-sizeAUTO
-
- File types it changes
-
.scss
gives the header icons, steppers and swatches a 24px minimum; no page reports small targets Example data Resources/app/storefront/src/scss/_variant-broken.scss Changes to Resources/app/storefront/src/scss/_variant-broken.scss: 6 added, 2 removed
Changes to Resources/app/storefront/src/scss/_variant-broken.scss: 6 added, 2 removed Line before Line after Change Code 10 10 11 11 .header-actions-btn { 12 12 width: 18px; 13 Added min-width: 24px; 13 14 height: 18px; 14 Removed min-height: 0; 15 Added min-height: 24px; 15 16 padding: 0; 16 17 } 17 18 96 97 /* 18 px colour swatches, 20 px quantity and remove buttons (vs-target-size). */ 97 98 .product-detail-configurator-option-input { 98 99 width: 18px; 100 Added min-width: 24px; 99 101 height: 18px; 102 Added min-height: 24px; 100 103 margin-right: 4px; 101 104 } 102 105 103 106 .quantity-selector-group .btn, 104 107 .line-item-remove-button { 105 108 width: 20px; 109 Added min-width: 24px; 106 110 height: 20px; 107 Removed min-height: 0; 111 Added min-height: 24px; 108 112 padding: 0; 109 113 } 110 114
Motion, slideshows and sound
-
Less motion when a visitor asks for it reduced-motion-guard When a visitor’s device asks for reduced motion, the animations and transitions that stylesheets define end at once and smooth scrolling is turned off.
When a visitor’s device is set to reduce motion, a block in the main stylesheet makes animations and transitions end at once and turns smooth scrolling off. The block applies to every element of the page, so it also covers animations that other stylesheets define, including those of plugins. Where the theme’s storefront script starts a timer, such as a slideshow’s autoplay, only when an autoplay option is set, that condition now also checks the visitor’s setting. Other motion that a script starts is listed for the reviewer, not guessed at.
- Rules it serves
-
-
vs-motionAUTO
-
- File types it changes
-
.js.scss
guards the pulsing badge and the slider autoplay; the animation stops under reduce Example data Resources/app/storefront/src/main.js Changes to Resources/app/storefront/src/main.js: 1 added, 1 removed
Changes to Resources/app/storefront/src/main.js: 1 added, 1 removed Line before Line after Change Code 139 139 }); 140 140 }); 141 141 142 Removed if (options.autoplay === true) { 142 Added if (options.autoplay === true && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 143 143 window.setInterval( 144 144 function () { 145 145 show(current + 1); Resources/app/storefront/src/scss/base.scss Changes to Resources/app/storefront/src/scss/base.scss: 14 added, 0 removed
Changes to Resources/app/storefront/src/scss/base.scss: 14 added, 0 removed Line before Line after Change Code 1708 1708 margin: 8px 0 0; 1709 1709 font-size: 0.875rem; 1710 1710 } 1711 Added 1712 Added /* validiscan:reduced-motion-guard */ 1713 Added /* Honour the visitor's reduced-motion setting: animations and transitions end at once, no smooth scrolling. */ 1714 Added @media (prefers-reduced-motion: reduce) { 1715 Added *, 1716 Added *::before, 1717 Added *::after { 1718 Added animation-duration: 0.01ms !important; 1719 Added animation-iteration-count: 1 !important; 1720 Added transition-duration: 0.01ms !important; 1721 Added scroll-behavior: auto !important; 1722 Added } 1723 Added } 1724 Added /* /validiscan:reduced-motion-guard */ -
A pause button for moving slideshows slider-pause-control Shopware’s base slider, behind the image slider of the CMS, gets a visible, translated button to pause and play it.
Shopware’s base slider, the slider behind the image slider of the CMS, gets a visible, translated button to pause and play it, next to its previous and next arrows. A block of code in the theme’s storefront script connects the button to the slider’s autoplay through Shopware’s own slider plugin. Where the theme runs the autoplay with its own timer, that timer gets one line that skips a step while the slider is paused. The button stays hidden until the script has connected it, so it never appears without an autoplay to pause. For visitors whose device asks for reduced motion, the slider starts paused. Sliders built with other libraries are left to a person.
- Rules it serves
-
-
vs-carouselAUTO
-
- File types it changes
-
.html.twig.js.json
adds a wired pause button to the hero slider; the carousel is no longer unstoppable Example data Resources/app/storefront/src/main.js Changes to Resources/app/storefront/src/main.js: 52 added, 0 removed
Changes to Resources/app/storefront/src/main.js: 52 added, 0 removed Line before Line after Change Code 142 142 if (options.autoplay === true) { 143 143 window.setInterval( 144 144 function () { 145 Added if (slider.getAttribute('data-autoplay-paused') === 'true') return; 145 146 show(current + 1); 146 147 }, 147 148 Number(options.autoplayTimeout) || 5000, 453 454 initFilterRanges(); 454 455 initToggles(); 455 456 })(); 457 Added 458 Added /* validiscan:slider-pause-control */ 459 Added /* 460 Added * Pause / play button of auto-advancing sliders (WCAG 2.2.2): wired to the 461 Added * slider's autoplay; sliders start paused when the visitor prefers reduced motion. 462 Added */ 463 Added (function sliderPauseControl() { 464 Added 'use strict'; 465 Added 466 Added function tinySlider(slider) { 467 Added const manager = window.PluginManager; 468 Added if (!manager || typeof manager.getPluginInstanceFromElement !== 'function') return null; 469 Added const plugin = manager.getPluginInstanceFromElement(slider, 'BaseSlider'); 470 Added return plugin && plugin._slider ? plugin._slider : null; 471 Added } 472 Added 473 Added function autoplays(slider) { 474 Added try { 475 Added const options = JSON.parse(slider.getAttribute('data-base-slider-options') || '{}') || {}; 476 Added return Boolean(options.slider && options.slider.autoplay); 477 Added } catch { 478 Added return false; 479 Added } 480 Added } 481 Added 482 Added function init() { 483 Added const reduceMotion = 484 Added window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 485 Added document.querySelectorAll('[data-slider-pause]').forEach(function (button) { 486 Added const slider = button.closest('[data-base-slider]'); 487 Added if (!slider || !autoplays(slider)) return; 488 Added 489 Added function setPaused(paused) { 490 Added slider.setAttribute('data-autoplay-paused', paused ? 'true' : 'false'); 491 Added button.textContent = button.getAttribute(paused ? 'data-label-play' : 'data-label-pause'); 492 Added const instance = tinySlider(slider); 493 Added if (instance) instance[paused ? 'pause' : 'play'](); 494 Added } 495 Added 496 Added button.hidden = false; 497 Added button.addEventListener('click', function () { 498 Added setPaused(slider.getAttribute('data-autoplay-paused') !== 'true'); 499 Added }); 500 Added if (reduceMotion) setPaused(true); 501 Added }); 502 Added } 503 Added 504 Added if (document.readyState === 'complete') init(); 505 Added else window.addEventListener('load', init); 506 Added })(); 507 Added /* /validiscan:slider-pause-control */ Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 6 added, 0 removed
Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 6 added, 0 removed Line before Line after Change Code 193 193 "notFoundText": "Die gesuchte Seite existiert nicht oder wurde verschoben.", 194 194 "backToHome": "Zurück zur Startseite", 195 195 "searchInstead": "Unsere Kaffees durchsuchen" 196 Added }, 197 Added "a11y": { 198 Added "slider": { 199 Added "pause": "Diashow anhalten", 200 Added "play": "Diashow abspielen" 201 Added } 196 202 } 197 203 } Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 6 added, 0 removed
Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 6 added, 0 removed Line before Line after Change Code 193 193 "notFoundText": "The page you are looking for does not exist or has moved.", 194 194 "backToHome": "Back to the homepage", 195 195 "searchInstead": "Search our coffees" 196 Added }, 197 Added "a11y": { 198 Added "slider": { 199 Added "pause": "Pause slideshow", 200 Added "play": "Play slideshow" 201 Added } 196 202 } 197 203 } Resources/views/storefront/element/cms-element-image-slider.html.twig Changes to Resources/views/storefront/element/cms-element-image-slider.html.twig: 1 added, 0 removed
Changes to Resources/views/storefront/element/cms-element-image-slider.html.twig: 1 added, 0 removed Line before Line after Change Code 57 57 <button type="button" class="base-slider-controls-next image-slider-controls-btn" data-slide-next> 58 58 {% sw_icon 'arrow-head-right' %} 59 59 </button> 60 Added <button type="button" class="btn btn-light base-slider-pause" data-slider-pause="true" data-label-pause="{{ 'a11y.slider.pause'|trans|striptags }}" data-label-play="{{ 'a11y.slider.play'|trans|striptags }}" hidden>{{ 'a11y.slider.pause'|trans|sw_sanitize }}</button> 60 61 </div> 61 62 {% endblock %} 62 63 </div> -
No blinking or scrolling text remove-blink-marquee
<blink>and<marquee>elements in the Twig templates become static elements with the same content, classes and ids, so no text moves on its own.<blink>and<marquee>elements in the Twig templates become static elements with the same content, classes and ids: a<span>for blink, a<div>for marquee, or a<span>where the marquee sits inside a paragraph or a similar text element. The marquee’s scrolling settings are removed, so no text moves on its own.- Rules it serves
-
-
blinkAUTO -
marqueeAUTO
-
- File types it changes
-
.html.twig
replaces a seeded marquee and blink in the top bar; axe no longer reports them Example data Resources/views/storefront/layout/header/top-bar.html.twig Changes to Resources/views/storefront/layout/header/top-bar.html.twig: 2 added, 2 removed
Changes to Resources/views/storefront/layout/header/top-bar.html.twig: 2 added, 2 removed Line before Line after Change Code 4 4 {% block layout_header_top_bar_usp %} 5 5 <p class="top-bar-usp"> 6 6 {% sw_icon 'checkmark' style { size: 'xs' } %} 7 Removed <marquee class="top-bar-marquee" behavior="scroll" scrollamount="4"><span>{{ 'header.topBarUsp'|trans }}</span></marquee> 7 Added <span class="top-bar-marquee"><span>{{ 'header.topBarUsp'|trans }}</span></span> 8 8 </p> 9 9 {% endblock %} 10 10 13 13 <li class="top-bar-nav-item"> 14 14 <a class="top-bar-nav-link top-bar-help" href="{{ url('/contact') }}"> 15 15 {% sw_icon 'headset' style { size: 'xs' } %} 16 Removed <blink class="top-bar-blink">{{ 'header.topBarHelp'|trans }}</blink> 16 Added <span class="top-bar-blink">{{ 'header.topBarHelp'|trans }}</span> 17 17 </a> 18 18 </li> 19 19 </ul> -
No sound that starts by itself mute-autoplay Video that plays on its own is muted and audio no longer starts by itself, except autoplay that template code or a script sets and players in frames; on Shopware the recipe is not used for
vs-mediafindings.A video that plays on its own gets
mutedandplaysinline, so it keeps playing silently, as a background video does, and plays inside the page in Safari on phones instead of opening the full-screen player. Audio that starts by itself no longer does; visitors start it themselves. Autoplay that template code or a script sets, and players embedded in frames, are left to a person. In a Validiscan scan, sound that starts by itself is reported by Validiscan’s media checkvs-media, which takes the place of the axe-core ruleno-autoplay-audio.vs-media, which also covers captions and transcripts, is MANUAL, so on Shopware such a finding comes with guidance rather than with this change.- Rules it serves
-
-
no-autoplay-audioAUTO checked through vs-media -
vs-mediaMANUAL
-
- File types it changes
-
.html.twig
mutes a seeded autoplaying background video; vs-media no longer reports autoplay Example data Resources/views/storefront/block/cms-block-video.html.twig Changes to Resources/views/storefront/block/cms-block-video.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/block/cms-block-video.html.twig: 1 added, 1 removed Line before Line after Change Code 9 9 <h2 class="cms-block-title">{{ block.title }}</h2> 10 10 <p class="cms-video-text">{{ block.text }}</p> 11 11 <div class="cms-element-video"> 12 Removed <video class="cms-video" autoplay loop preload="none" width="1280" height="720" poster="{{ block.posterUrl }}"> 12 Added <video class="cms-video" autoplay loop preload="none" width="1280" height="720" poster="{{ block.posterUrl }}" muted playsinline> 13 13 <source src="{{ block.videoUrl }}" type="video/mp4"> 14 14 {% if block.captionsUrl %} 15 15 <track kind="captions" src="{{ block.captionsUrl }}" srclang="en" label="{{ 'general.captionsEnglish'|trans }}" default>
Forms, the cart and embedded content
-
Labels connected to their fields label-association A field whose label text is already on the page, right before it, gets connected to that text.
A form field whose label text is already on the page, right before the field, gets connected to that text. A
<label>without aforgets one that points at the field. A text element that the theme marks as a title or label by its class, such as the name of a variant group on a product page, becomes the field’s<label>, keeping its classes and its own line. Screen readers then read the label when the field gets focus. A field that only has a placeholder is left to a person, because writing a label is a content decision.- Rules it serves
-
-
labelAUTO checked through vs-forms-ecommerce -
select-nameASSISTED -
vs-forms-ecommerceAUTO
-
- File types it changes
-
.html.twig
turns the variant group titles into labels of their selects; select-name no longer fires Example data Resources/views/storefront/page/product-detail/configurator.html.twig Changes to Resources/views/storefront/page/product-detail/configurator.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/page/product-detail/configurator.html.twig: 1 added, 1 removed Line before Line after Change Code 13 13 {% for group in product.configurator %} 14 14 {% if group.display == 'select' %} 15 15 <div class="product-detail-configurator-group"> 16 Removed <div class="product-detail-configurator-group-title">{{ group.name }}</div> 16 Added <label class="product-detail-configurator-group-title" for="configurator-{{ group.id }}">{{ group.name }}</label> 17 17 <select class="form-select product-detail-configurator-select-input" 18 18 id="configurator-{{ group.id }}" 19 19 name="{{ group.id }}"{% if group.price|default(false) %} -
Account, address and contact fields that browsers can fill in autocomplete-tokens Fields that ask for the customer’s own data, in forms such as registration, address and login, get the
autocompletevalue for what they ask for, unless template code writes it.Fields that ask for the customer’s own data, such as those of the registration, address, login, contact and newsletter forms, get the
autocompletevalue for what they ask for, such as email, first and last name, postal code, phone number or password. The value is worked out from the field’s type, name, id, label and placeholder. Passwords getnew-passwordin registration forms andcurrent-passwordelsewhere. Search, voucher, quantity, captcha and message fields are left as they are, and a value that template code writes is left to a person. Browsers can then fill the fields in, and assistive technology can tell what each one is for.- Rules it serves
-
-
autocomplete-validAUTO checked through vs-forms-ecommerce -
vs-forms-ecommerceAUTO
-
- File types it changes
-
.html.twig
adds tokens to the register, address, login, contact and newsletter fields; no autocomplete hits remain Example data Resources/views/storefront/component/account/login.html.twig Changes to Resources/views/storefront/component/account/login.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/component/account/login.html.twig: 1 added, 1 removed Line before Line after Change Code 19 19 name="username" 20 20 placeholder="{{ 'account.loginMailPlaceholder'|trans }}" 21 21 required 22 Removed autocomplete="off"> 22 Added autocomplete="email"> 23 23 </div> 24 24 25 25 <div class="form-group"> Resources/views/storefront/component/account/register.html.twig Changes to Resources/views/storefront/component/account/register.html.twig: 8 added, 4 removed
Changes to Resources/views/storefront/component/account/register.html.twig: 8 added, 4 removed Line before Line after Change Code 33 33 id="{{ formId }}FirstName" 34 34 name="firstName" 35 35 placeholder="{{ 'account.firstNamePlaceholder'|trans }}" 36 Removed required> 36 Added required 37 Added autocomplete="given-name"> 37 38 </div> 38 39 <div class="form-group col-sm-6"> 39 40 <input type="text" 41 42 id="{{ formId }}LastName" 42 43 name="lastName" 43 44 placeholder="{{ 'account.lastNamePlaceholder'|trans }}" 44 Removed required> 45 Added required 46 Added autocomplete="family-name"> 45 47 </div> 46 48 </div> 47 49 <div class="row g-2"> 52 54 id="{{ formId }}Mail" 53 55 name="email" 54 56 placeholder="{{ 'account.emailPlaceholder'|trans }}" 55 Removed required> 57 Added required 58 Added autocomplete="email"> 56 59 </div> 57 60 <div class="form-group col-sm-6"> 58 61 <label class="form-label" for="{{ formId }}Password">{{ 'account.passwordLabel'|trans }}</label> 62 65 name="password" 63 66 minlength="8" 64 67 aria-describedby="{{ formId }}PasswordHint" 65 Removed required> 68 Added required 69 Added autocomplete="new-password"> 66 70 <small class="form-text" id="{{ formId }}PasswordHint">{{ 'account.passwordHint'|trans }}</small> 67 71 </div> 68 72 </div> Resources/views/storefront/component/address/address-form.html.twig Changes to Resources/views/storefront/component/address/address-form.html.twig: 8 added, 4 removed
Changes to Resources/views/storefront/component/address/address-form.html.twig: 8 added, 4 removed Line before Line after Change Code 14 14 id="{{ prefix }}AddressStreet" 15 15 name="{{ prefix }}[street]" 16 16 placeholder="{{ 'address.streetPlaceholder'|trans }}" 17 Removed required> 17 Added required 18 Added autocomplete="address-line1"> 18 19 </div> 19 20 {% endblock %} 20 21 26 27 name="{{ prefix }}[zipcode]" 27 28 placeholder="{{ 'address.zipcodePlaceholder'|trans }}" 28 29 inputmode="numeric" 29 Removed required> 30 Added required 31 Added autocomplete="postal-code"> 30 32 </div> 31 33 {% endblock %} 32 34 37 39 class="form-control" 38 40 id="{{ prefix }}AddressCity" 39 41 name="{{ prefix }}[city]" 40 Removed required> 42 Added required 43 Added autocomplete="address-level2"> 41 44 </div> 42 45 {% endblock %} 43 46 47 50 <select class="form-select" 48 51 id="{{ prefix }}AddressCountry" 49 52 name="{{ prefix }}[countryId]" 50 Removed required> 53 Added required 54 Added autocomplete="country"> 51 55 {% for country in countries %} 52 56 <option value="{{ country.id }}"{% if loop.first %} selected{% endif %}>{{ country.name }}</option> 53 57 {% endfor %} Resources/views/storefront/element/cms-element-form.html.twig Changes to Resources/views/storefront/element/cms-element-form.html.twig: 4 added, 4 removed
Changes to Resources/views/storefront/element/cms-element-form.html.twig: 4 added, 4 removed Line before Line after Change Code 18 18 </select> 19 19 </div> 20 20 <div class="form-group col-md-4"> 21 Removed <input type="text" class="form-control" id="contactFormFirstName" name="firstName" placeholder="{{ 'account.firstNamePlaceholder'|trans }}" required> 21 Added <input type="text" class="form-control" id="contactFormFirstName" name="firstName" placeholder="{{ 'account.firstNamePlaceholder'|trans }}" required autocomplete="given-name"> 22 22 </div> 23 23 <div class="form-group col-md-4"> 24 Removed <input type="text" class="form-control" id="contactFormLastName" name="lastName" placeholder="{{ 'account.lastNamePlaceholder'|trans }}" required> 24 Added <input type="text" class="form-control" id="contactFormLastName" name="lastName" placeholder="{{ 'account.lastNamePlaceholder'|trans }}" required autocomplete="family-name"> 25 25 </div> 26 26 </div> 27 27 28 28 <div class="row g-2"> 29 29 <div class="form-group col-md-6"> 30 30 <label class="form-label" for="contactFormMail">{{ 'account.emailLabel'|trans }}</label> 31 Removed <input type="email" class="form-control" id="contactFormMail" name="email" placeholder="{{ 'account.emailPlaceholder'|trans }}" required> 31 Added <input type="email" class="form-control" id="contactFormMail" name="email" placeholder="{{ 'account.emailPlaceholder'|trans }}" required autocomplete="email"> 32 32 </div> 33 33 <div class="form-group col-md-6"> 34 34 <label class="form-label" for="contactFormPhone">{{ 'contact.phoneLabel'|trans }}</label> 35 Removed <input type="tel" class="form-control" id="contactFormPhone" name="phone"> 35 Added <input type="tel" class="form-control" id="contactFormPhone" name="phone" autocomplete="tel"> 36 36 </div> 37 37 </div> 38 38 Resources/views/storefront/layout/footer/footer.html.twig Changes to Resources/views/storefront/layout/footer/footer.html.twig: 2 added, 1 removed
Changes to Resources/views/storefront/layout/footer/footer.html.twig: 2 added, 1 removed Line before Line after Change Code 62 62 id="footerNewsletterMail" 63 63 class="form-control" 64 64 placeholder="{{ 'footer.newsletterPlaceholder'|trans }}" 65 Removed required> 65 Added required 66 Added autocomplete="email"> 66 67 <button type="submit" class="btn btn-primary newsletter-submit">{{ 'footer.newsletterSubmit'|trans }}</button> 67 68 </div> 68 69 <div class="form-check newsletter-privacy"> -
Price, stock and cart changes announced live-region-cart-variant The price, availability or cart totals that the storefront script updates in place become polite live regions, unless the script replaces the element itself.
The parts of a page that the storefront script updates in place, such as the price after a customer picks a variant, the availability or the cart totals, become polite live regions. Screen readers then read the new value once, in full, without interrupting. Where a script replaces the element itself instead of its content, a live region would not work, so the recipe leaves that element to a person.
- Rules it serves
-
-
vs-live-updatesAUTO
-
- File types it changes
-
.html.twig
makes the variant price and the cart totals polite live regions; the updates are announced Example data Resources/views/storefront/page/checkout/cart/index.html.twig Changes to Resources/views/storefront/page/checkout/cart/index.html.twig: 2 added, 2 removed
Changes to Resources/views/storefront/page/checkout/cart/index.html.twig: 2 added, 2 removed Line before Line after Change Code 59 59 <span class="line-item-unit-price-label">{{ 'checkout.cartHeaderUnitPrice'|trans }}:</span> 60 60 <span class="line-item-unit-price-value">{{ item.unitPrice|currency }}*</span> 61 61 </div> 62 Removed <div class="line-item-total-price"> 62 Added <div class="line-item-total-price" aria-live="polite" aria-atomic="true"> 63 63 <span class="line-item-total-price-label">{{ 'checkout.cartHeaderTotalPrice'|trans }}:</span> 64 64 <span class="line-item-total-price-value" data-line-total>{{ item.totalPrice|currency }}*</span> 65 65 </div> 88 88 <div class="col-lg-4 checkout-aside"> 89 89 <div class="checkout-aside-summary"> 90 90 <h2 class="checkout-aside-summary-header">{{ 'checkout.summaryHeader'|trans }}</h2> 91 Removed <div class="checkout-aside-summary-totals" data-shipping-threshold="{{ shop.shippingThreshold }}" data-shipping-costs="{{ shop.shippingCosts }}"> 91 Added <div class="checkout-aside-summary-totals" data-shipping-threshold="{{ shop.shippingThreshold }}" data-shipping-costs="{{ shop.shippingCosts }}" aria-live="polite" aria-atomic="true"> 92 92 <dl class="checkout-aside-summary-list"> 93 93 <dt class="checkout-aside-summary-label">{{ 'checkout.summaryPositionPrice'|trans }}</dt> 94 94 <dd class="checkout-aside-summary-value" data-cart-subtotal>{{ cart.subtotal|currency }}*</dd> Resources/views/storefront/page/product-detail/buy-widget.html.twig Changes to Resources/views/storefront/page/product-detail/buy-widget.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/page/product-detail/buy-widget.html.twig: 1 added, 1 removed Line before Line after Change Code 17 17 {% endblock %} 18 18 19 19 {% block buy_widget_price %} 20 Removed <div class="product-detail-price-container"> 20 Added <div class="product-detail-price-container" aria-live="polite" aria-atomic="true"> 21 21 <meta itemprop="priceCurrency" content="{{ shop.currency.isoCode }}"> 22 22 <meta itemprop="price" content="{{ product.price }}"> 23 23 <p class="product-detail-price{% if product.listPrice %} with-list-price{% endif %}" data-price-display>{{ product.price|currency }}*</p> -
Titles for embedded maps and videos iframe-title An embedded frame without a title gets a translated title that says what it shows, where the code reveals it.
An embedded frame without a title, such as a map or a YouTube or Vimeo video, gets a translated title from the theme’s snippet files that says what it shows. The recipe reads what the frame shows from its address, its class and name, or the template around it. A frame whose content the code does not reveal gets no title from the recipe, because an invented title is worse than none.
- Rules it serves
-
-
frame-titleAUTO
-
- File types it changes
-
.html.twig.json
titles the contact map frame through the snippets; frame-title no longer fires Example data Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 5 added, 0 removed
Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 5 added, 0 removed Line before Line after Change Code 193 193 "notFoundText": "Die gesuchte Seite existiert nicht oder wurde verschoben.", 194 194 "backToHome": "Zurück zur Startseite", 195 195 "searchInstead": "Unsere Kaffees durchsuchen" 196 Added }, 197 Added "a11y": { 198 Added "iframe": { 199 Added "map": "Karte" 200 Added } 196 201 } 197 202 } Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 5 added, 0 removed
Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 5 added, 0 removed Line before Line after Change Code 193 193 "notFoundText": "The page you are looking for does not exist or has moved.", 194 194 "backToHome": "Back to the homepage", 195 195 "searchInstead": "Search our coffees" 196 Added }, 197 Added "a11y": { 198 Added "iframe": { 199 Added "map": "Map" 200 Added } 196 201 } 197 202 } Resources/views/storefront/block/cms-block-map.html.twig Changes to Resources/views/storefront/block/cms-block-map.html.twig: 2 added, 1 removed
Changes to Resources/views/storefront/block/cms-block-map.html.twig: 2 added, 1 removed Line before Line after Change Code 11 11 <iframe class="cms-map-frame" 12 12 src="{{ url('/widgets/cms/map') }}" 13 13 width="1280" 14 Removed height="360"></iframe> 14 Added height="360" 15 Added title="{{ 'a11y.iframe.map'|trans|striptags }}"></iframe> 15 16 </div> 16 17 <p class="cms-map-address">{{ shop.company.street }}, {{ shop.company.zip }} {{ shop.company.city }}</p> 17 18 </div>
Contrast, images and controls that need judgement
Text contrast, text alternatives for images and names for unnamed controls usually need judgement: which colour fits your brand, which words describe an image. The rules that report them have the tier ASSISTED, as the lists below show. For a finding of an ASSISTED rule, AI drafts a change only if you agreed to AI drafting, and Validiscan reviews every change before it is delivered.
The four recipes below are not the standard fix of any rule. Each one covers only the narrow cases in which the theme’s code alone proves what the change must be. Two of them, button-semantics and icon-accessible-name, also serve the slideshow check vs-carousel, which is AUTO: in a project the fix engine tries them on its findings when the pause button recipe above cannot fix them. The other two serve only rules of tier ASSISTED.
-
Text colours with enough contrast contrast-tokens A failing text colour defined in exactly one place moves to the nearest shade of its hue with the required contrast; not applied in a project.
When text fails the contrast check and its colour is defined in exactly one place in the theme, that place changes to the nearest shade of the same hue that reaches the required contrast, so the design stays recognisable. One place can be a colour field in
theme.jsontogether with the SCSS variable that repeats it, an SCSS variable or custom property used only for text, or a singlecolordeclaration. A colour that is used in several places or also as a background or border, and Shopware’s core colour fields for the brand, borders, backgrounds, status messages and the buy button, are left to a person. A colour saved in the Administration’s theme settings overridestheme.json: if it was changed there, it has to be changed there too. The rulecolor-contrasthas the tier ASSISTED, so in a project a contrast finding takes the ASSISTED way and this recipe is not applied to it.- Rules it serves
-
-
color-contrastASSISTED
-
- File types it changes
-
.scss
darkens the unit price colour to 4.5:1 with the same hue; axe clears it on both pages Example data Resources/app/storefront/src/scss/_variant-broken.scss Changes to Resources/app/storefront/src/scss/_variant-broken.scss: 1 added, 1 removed
Changes to Resources/app/storefront/src/scss/_variant-broken.scss: 1 added, 1 removed Line before Line after Change Code 45 45 /* Light grey unit prices, 3.1:1 (color-contrast). */ 46 46 .product-price-unit, 47 47 .product-detail-price-unit { 48 Removed color: #929292; 48 Added color: #757575; 49 49 } 50 50 51 51 /* Fixed two-line product names (vs-text-spacing). */ -
Decorative images hidden from screen readers decorative-image-alt Only images the template proves decorative get an empty
altoraria-hidden; not applied in a project.An image is marked as decorative only when the template proves it adds nothing: it sits in a link or button that is named another way, it is an icon next to its own text label, or it is a spacer or divider. An
<img>gets an emptyalt, an image printed with Shopware’ssw_thumbnailstag gets an emptyaltin its attributes, and SVG icons getaria-hidden="true", so screen readers skip them instead of reading out a file name. Every other image is content, such as the payment logos in a footer, and its text alternative needs words a person chooses. The rules this recipe serves have the tier ASSISTED, so in a project their findings take the ASSISTED way and this recipe is not applied to them.- Rules it serves
-
-
image-altASSISTED -
role-img-altASSISTED -
svg-img-altASSISTED
-
- File types it changes
-
.html.twig
marks a seeded phone icon inside the hotline link decorative; image-alt stops firing there Example data Resources/views/storefront/layout/footer/footer.html.twig Changes to Resources/views/storefront/layout/footer/footer.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/layout/footer/footer.html.twig: 1 added, 1 removed Line before Line after Change Code 9 9 <div class="footer-column-content"> 10 10 <p class="footer-contact-hotline"> 11 11 {{ 'footer.serviceHotlineText'|trans }} 12 Removed <a class="footer-contact-hotline-link" href="tel:+49405550192"><img src="{{ asset('bundles/storefront/assets/icon/default/headset.svg') }}" class="footer-contact-hotline-icon" width="16" height="16"> {{ shop.company.phone }}</a> 12 Added <a class="footer-contact-hotline-link" href="tel:+49405550192"><img src="{{ asset('bundles/storefront/assets/icon/default/headset.svg') }}" class="footer-contact-hotline-icon" width="16" height="16" alt=""> {{ shop.company.phone }}</a> 13 13 </p> 14 14 <p class="footer-contact-hours">{{ 'footer.serviceHours'|trans }}</p> 15 15 <p class="footer-contact-form"> -
Names for icon-only buttons and links icon-accessible-name Icon-only buttons and links get a translated name that says what they do, where the code reveals their purpose; in a project the recipe is tried on the slideshow controls that
vs-carouselreports.Buttons and links that show only an icon, such as search, cart, account, the logo link, slider arrows, quantity buttons and cookie choices, get a translated
aria-labelthat says what they do. The purpose is read from the control’s classes, its link target, the icon inside it and the component around it. A control whose purpose the code does not reveal, such as a social link whose network is stored as data, is left to a person, because an invented name is worse than none. In a project this recipe is tried on the slideshow controls thatvs-carouselreports; other icon-only controls are reported by rules of tier ASSISTED and take the ASSISTED way.- Rules it serves
-
-
button-nameASSISTED -
link-nameASSISTED -
vs-carouselAUTO -
vs-cookie-bannerASSISTED
-
- File types it changes
-
.html.twig.json
names every icon-only button of the home, product and cart pages; button-name no longer fires Example data Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 12 added, 0 removed
Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 12 added, 0 removed Line before Line after Change Code 193 193 "notFoundText": "Die gesuchte Seite existiert nicht oder wurde verschoben.", 194 194 "backToHome": "Zurück zur Startseite", 195 195 "searchInstead": "Unsere Kaffees durchsuchen" 196 Added }, 197 Added "a11y": { 198 Added "control": { 199 Added "search": "Suchen", 200 Added "previousSlide": "Vorherige Folie", 201 Added "nextSlide": "Nächste Folie", 202 Added "cookieSettings": "Cookie-Einstellungen", 203 Added "acceptCookies": "Alle Cookies akzeptieren", 204 Added "decreaseQuantity": "Menge verringern", 205 Added "increaseQuantity": "Menge erhöhen", 206 Added "removeFromCart": "Aus dem Warenkorb entfernen" 207 Added } 196 208 } 197 209 } Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 12 added, 0 removed
Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 12 added, 0 removed Line before Line after Change Code 193 193 "notFoundText": "The page you are looking for does not exist or has moved.", 194 194 "backToHome": "Back to the homepage", 195 195 "searchInstead": "Search our coffees" 196 Added }, 197 Added "a11y": { 198 Added "control": { 199 Added "search": "Search", 200 Added "previousSlide": "Previous slide", 201 Added "nextSlide": "Next slide", 202 Added "cookieSettings": "Cookie settings", 203 Added "acceptCookies": "Accept all cookies", 204 Added "decreaseQuantity": "Decrease quantity", 205 Added "increaseQuantity": "Increase quantity", 206 Added "removeFromCart": "Remove from cart" 207 Added } 196 208 } 197 209 } Resources/views/storefront/element/cms-element-image-gallery.html.twig Changes to Resources/views/storefront/element/cms-element-image-gallery.html.twig: 2 added, 2 removed
Changes to Resources/views/storefront/element/cms-element-image-gallery.html.twig: 2 added, 2 removed Line before Line after Change Code 31 31 32 32 {% if media|length > 1 %} 33 33 <div class="gallery-slider-controls"> 34 Removed <button type="button" class="base-slider-controls-prev gallery-slider-controls-prev" data-gallery-prev> 34 Added <button type="button" class="base-slider-controls-prev gallery-slider-controls-prev" data-gallery-prev aria-label="{{ 'a11y.control.previousSlide'|trans|striptags }}"> 35 35 {% sw_icon 'arrow-head-left' %} 36 36 </button> 37 Removed <button type="button" class="base-slider-controls-next gallery-slider-controls-next" data-gallery-next> 37 Added <button type="button" class="base-slider-controls-next gallery-slider-controls-next" data-gallery-next aria-label="{{ 'a11y.control.nextSlide'|trans|striptags }}"> 38 38 {% sw_icon 'arrow-head-right' %} 39 39 </button> 40 40 </div> Resources/views/storefront/element/cms-element-image-slider.html.twig Changes to Resources/views/storefront/element/cms-element-image-slider.html.twig: 2 added, 2 removed
Changes to Resources/views/storefront/element/cms-element-image-slider.html.twig: 2 added, 2 removed Line before Line after Change Code 46 46 47 47 {% block element_image_slider_controls %} 48 48 <div class="image-slider-controls-container"> 49 Removed <button type="button" class="base-slider-controls-prev image-slider-controls-btn" data-slide-prev> 49 Added <button type="button" class="base-slider-controls-prev image-slider-controls-btn" data-slide-prev aria-label="{{ 'a11y.control.previousSlide'|trans|striptags }}"> 50 50 {% sw_icon 'arrow-head-left' %} 51 51 </button> 52 52 <ol class="base-slider-dots"> 54 54 <li class="base-slider-dot{% if loop.first %} is-active{% endif %}" data-slide-to="{{ loop.index0 }}"></li> 55 55 {% endfor %} 56 56 </ol> 57 Removed <button type="button" class="base-slider-controls-next image-slider-controls-btn" data-slide-next> 57 Added <button type="button" class="base-slider-controls-next image-slider-controls-btn" data-slide-next aria-label="{{ 'a11y.control.nextSlide'|trans|striptags }}"> 58 58 {% sw_icon 'arrow-head-right' %} 59 59 </button> 60 60 </div> Resources/views/storefront/layout/cookie/cookie-permission.html.twig Changes to Resources/views/storefront/layout/cookie/cookie-permission.html.twig: 2 added, 2 removed
Changes to Resources/views/storefront/layout/cookie/cookie-permission.html.twig: 2 added, 2 removed Line before Line after Change Code 22 22 </p> 23 23 </div> 24 24 <div class="col-auto cookie-permission-actions"> 25 Removed <button type="button" class="btn cookie-permission-icon-btn js-cookie-configuration-toggle">{% sw_icon 'settings' %}</button> 26 Removed <button type="button" class="btn btn-primary cookie-permission-icon-btn js-cookie-accept-all-button">{% sw_icon 'checkmark' %}</button> 25 Added <button type="button" class="btn cookie-permission-icon-btn js-cookie-configuration-toggle" aria-label="{{ 'a11y.control.cookieSettings'|trans|striptags }}">{% sw_icon 'settings' %}</button> 26 Added <button type="button" class="btn btn-primary cookie-permission-icon-btn js-cookie-accept-all-button" aria-label="{{ 'a11y.control.acceptCookies'|trans|striptags }}">{% sw_icon 'checkmark' %}</button> 27 27 </div> 28 28 </div> 29 29 <p class="cookie-permission-configuration" hidden>{{ 'cookie.configurationText'|trans }}</p> Resources/views/storefront/layout/header/search.html.twig Changes to Resources/views/storefront/layout/header/search.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/layout/header/search.html.twig: 1 added, 1 removed Line before Line after Change Code 13 13 autocomplete="off" 14 14 autocapitalize="off" 15 15 placeholder="{{ 'header.searchPlaceholder'|trans }}" value="{{ page.searchTerm }}"> 16 Removed <button type="submit" class="btn header-search-btn"> 16 Added <button type="submit" class="btn header-search-btn" aria-label="{{ 'a11y.control.search'|trans|striptags }}"> 17 17 <span class="header-search-icon">{% sw_icon 'search' %}</span> 18 18 </button> 19 19 </div> Resources/views/storefront/page/checkout/cart/index.html.twig Changes to Resources/views/storefront/page/checkout/cart/index.html.twig: 3 added, 3 removed
Changes to Resources/views/storefront/page/checkout/cart/index.html.twig: 3 added, 3 removed Line before Line after Change Code 39 39 </div> 40 40 <div class="line-item-quantity"> 41 41 <div class="input-group quantity-selector-group" data-quantity-selector="true"> 42 Removed <button type="button" class="btn btn-outline-light btn-minus js-btn-minus"> 42 Added <button type="button" class="btn btn-outline-light btn-minus js-btn-minus" aria-label="{{ 'a11y.control.decreaseQuantity'|trans|striptags }}"> 43 43 {% sw_icon 'minus' style { size: 'xs' } %} 44 44 </button> 45 45 <input type="number" 50 50 max="100" 51 51 step="1" 52 52 value="{{ item.quantity }}"> 53 Removed <button type="button" class="btn btn-outline-light btn-plus js-btn-plus"> 53 Added <button type="button" class="btn btn-outline-light btn-plus js-btn-plus" aria-label="{{ 'a11y.control.increaseQuantity'|trans|striptags }}"> 54 54 {% sw_icon 'plus' style { size: 'xs' } %} 55 55 </button> 56 56 </div> 64 64 <span class="line-item-total-price-value" data-line-total>{{ item.totalPrice|currency }}*</span> 65 65 </div> 66 66 <div class="line-item-remove"> 67 Removed <button type="button" class="btn btn-light btn-sm line-item-remove-button"> 67 Added <button type="button" class="btn btn-light btn-sm line-item-remove-button" aria-label="{{ 'a11y.control.removeFromCart'|trans|striptags }}"> 68 68 {% sw_icon 'x' style { size: 'xs' } %} 69 69 </button> 70 70 </div> Resources/views/storefront/page/product-detail/buy-widget.html.twig Changes to Resources/views/storefront/page/product-detail/buy-widget.html.twig: 2 added, 2 removed
Changes to Resources/views/storefront/page/product-detail/buy-widget.html.twig: 2 added, 2 removed Line before Line after Change Code 64 64 <div class="buy-widget-container"> 65 65 <div class="buy-widget-quantity"> 66 66 <div class="input-group quantity-selector-group" data-quantity-selector="true"> 67 Removed <button type="button" class="btn btn-outline-light btn-minus js-btn-minus"> 67 Added <button type="button" class="btn btn-outline-light btn-minus js-btn-minus" aria-label="{{ 'a11y.control.decreaseQuantity'|trans|striptags }}"> 68 68 {% sw_icon 'minus' style { size: 'xs' } %} 69 69 </button> 70 70 <input type="number" 75 75 max="100" 76 76 step="1" 77 77 value="1"> 78 Removed <button type="button" class="btn btn-outline-light btn-plus js-btn-plus"> 78 Added <button type="button" class="btn btn-outline-light btn-plus js-btn-plus" aria-label="{{ 'a11y.control.increaseQuantity'|trans|striptags }}"> 79 79 {% sw_icon 'plus' style { size: 'xs' } %} 80 80 </button> 81 81 </div> names the logo and account links; social links whose network is data are left for review Example data Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 6 added, 0 removed
Changes to Resources/snippet/de_DE/kaffeekontor-nord.de-DE.json: 6 added, 0 removed Line before Line after Change Code 193 193 "notFoundText": "Die gesuchte Seite existiert nicht oder wurde verschoben.", 194 194 "backToHome": "Zurück zur Startseite", 195 195 "searchInstead": "Unsere Kaffees durchsuchen" 196 Added }, 197 Added "a11y": { 198 Added "control": { 199 Added "home": "Zur Startseite", 200 Added "account": "Ihr Konto" 201 Added } 196 202 } 197 203 } Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 6 added, 0 removed
Changes to Resources/snippet/en_GB/kaffeekontor-nord.en-GB.json: 6 added, 0 removed Line before Line after Change Code 193 193 "notFoundText": "The page you are looking for does not exist or has moved.", 194 194 "backToHome": "Back to the homepage", 195 195 "searchInstead": "Search our coffees" 196 Added }, 197 Added "a11y": { 198 Added "control": { 199 Added "home": "Go to the homepage", 200 Added "account": "Your account" 201 Added } 196 202 } 197 203 } Resources/views/storefront/layout/header/actions.html.twig Changes to Resources/views/storefront/layout/header/actions.html.twig: 2 added, 1 removed
Changes to Resources/views/storefront/layout/header/actions.html.twig: 2 added, 1 removed Line before Line after Change Code 3 3 {% block layout_header_actions_account %} 4 4 <div class="header-account"> 5 5 <a href="{{ url('/account/login') }}" 6 Removed class="btn account-menu-btn header-actions-btn"> 6 Added class="btn account-menu-btn header-actions-btn" 7 Added aria-label="{{ 'a11y.control.account'|trans|striptags }}"> 7 8 {% sw_icon 'avatar' %} 8 9 </a> 9 10 </div> Resources/views/storefront/layout/header/logo.html.twig Changes to Resources/views/storefront/layout/header/logo.html.twig: 1 added, 1 removed
Changes to Resources/views/storefront/layout/header/logo.html.twig: 1 added, 1 removed Line before Line after Change Code 2 2 <div class="header-logo-main"> 3 3 {% block layout_header_logo_link %} 4 4 {# The agency removed the default title="Go to homepage" and the image alt to "avoid duplicate announcements". #} 5 Removed <a class="header-logo-main-link" href="{{ url('/') }}"> 5 Added <a class="header-logo-main-link" href="{{ url('/') }}" aria-label="{{ 'a11y.control.home'|trans|striptags }}"> 6 6 {% block layout_header_logo_image %} 7 7 <picture class="header-logo-picture"> 8 8 <img src="{{ asset('bundles/storefront/assets/logo/kaffeekontor-nord-logo.svg') }}"
Details: the tiers, the test theme, the checks on each change, and text in your shop’s languages
A recipe is a deterministic change for one kind of failure: the same code always gets the same change. Validiscan has 20 recipes for Shopware themes. Each one makes a change only where the theme’s code shows what the right change is. Where it does not, the recipe changes nothing. It does not guess.
Under each recipe you find the rules it serves, with each rule’s tier in Validiscan’s catalogue for Shopware. AUTO means a recipe is the rule’s standard fix. ASSISTED means the fix needs judgement, so a change is drafted and then reviewed. MANUAL means the rule needs a person’s decision and comes with guidance, not with a code change. In a project, recipes are applied to findings of rules of tier AUTO. Where a recipe also lists a rule of tier ASSISTED or MANUAL, a finding of that rule takes the ASSISTED or MANUAL way instead. A rule that is checked through another check does not run on its own in a scan; the other check reports the same failure.
The file types and the before and after of each recipe come from Validiscan’s own tests. They run on Validiscan’s Shopware test theme, a theme plugin on Shopware 6’s default storefront with accessibility failures put in on purpose. Each test renders the theme’s templates into pages before and after the change and runs the same rule on both: the failure must be reported before and no longer after, and the change must not cause a new failure of that rule. The pages are rendered by a test renderer, not by a full Shopware installation, and your theme’s files will look different.
Before Validiscan reviews a change, every changed file is checked. Twig templates are parsed with Shopware’s own tags, such as sw_extends and sw_include, known to the parser; they are never compiled or run. Stylesheets get a structure check, snippet files and theme.json are parsed as JSON, and JavaScript gets a syntax check.
When a recipe adds visible text or a name, such as the words of a skip link or the name of an icon button, it uses a snippet in the template. The text goes into the theme’s English snippet file, which the recipe creates if the theme has none, and into its German snippet file where the theme has one. Snippets that already exist keep their text. Text for the other languages of your shop is not added.
What stays MANUAL on Shopware?
Failures that need a decision about content or behaviour that a fixed code change cannot make, such as captions and transcripts for your videos and audio, stay MANUAL on Shopware. For a finding of a MANUAL rule, Validiscan gives guidance on how to fix it, not a change to your code.
The rules below run in scans and have the tier MANUAL on Shopware.
-
meta-refreshDelayed refresh under 20 hours must not be used -
nested-interactiveInteractive controls must not be nested -
scrollable-region-focusableScrollable region must have keyboard access -
server-side-image-mapServer-side image maps must not be used -
vs-keyboard-trapKeyboard focus must never get trapped -
vs-focus-orderFocus order must follow the visual order -
vs-draggingDrag-only controls need a single-pointer alternative -
vs-mediaVideo needs captions, audio needs a transcript, and nothing may autoplay with sound -
vs-documentsLinked documents need a manual accessibility review
Details: what a MANUAL finding gets, and what a project leaves out
For a finding of a MANUAL rule, Validiscan gives guidance on how to fix it, not a change to your code. These failures need a decision about content or behaviour that a fixed code change cannot make, for example captions and transcripts for your videos and audio, a way out of a widget that traps keyboard focus, or a second way to use a control that only works by dragging.
The work on MANUAL findings that need your own content decisions is not part of a project, and neither are PDFs. What the scan tests automatically and what is checked by hand
What do you provide?
You provide permission for a full scan, one way into your theme plugin, your answer on whether AI may draft ASSISTED fixes, a developer who reviews, builds and deploys the changes, some Administration settings, new text in languages other than English and German, and your decisions on what stays MANUAL.
- Your permission for a full scan of your shop, which Validiscan records before the scan. A full scan puts a product in the cart and opens the first checkout step, without completing a purchase or entering payment data.
- One way into your theme: access to the GitHub repository that holds the theme plugin, or a ZIP of the plugin’s folder under
custom/plugins, uploaded in the customer portal. - Your answer on whether AI may draft fixes of tier ASSISTED. AI drafting is off until you agree.
- A developer who reviews the changes, merges the pull request or applies the patch bundle, builds the theme and deploys it.
- The settings in the Shopware Administration that the fixes do not change: meta titles for pages that have none, the language of content written in another language than your shop, and a colour saved in the theme settings when a contrast fix changes that colour.
- Text for the languages of your shop other than English and German, where a fix adds new text.
- Your decisions on what stays MANUAL, such as captions for your videos.
Details: after the changes are live, and what one project covers
Once the changes are live, a verification scan checks the affected pages again, and the Remediation Evidence Package records the result. How a project runs, step by step
A project covers one shop: one domain and one theme. It is for a storefront built on a theme; a headless storefront is on request.
Sources
2 sources, each checked for this page on 6 October 2026.
Shopware’s developer documentation 2
- Add SCSS Styling and JavaScript to a Theme · Shopware · checked
- Add custom JavaScript · Shopware · checked
Start with a free check of your shop.
Send your shop’s address. Validiscan scans it and replies with every failure the scan can prove, with the page and the code.
Request a free checkOr write to info@validiscan.com.
What the free check covers, and what it leaves out- You send
- your shop’s address
- Pages scanned
- up to 15
- You get
- every failure the scan can prove, with the page and the code
- On your shop
- public pages only; no login, nothing bought, no forms sent
- What it is not
- a full audit