Accessibility fixes in your Magento theme’s code

Validiscan fixes accessibility failures in your Magento 2 theme’s .phtml templates, layout XML and LESS or CSS stylesheets. Nothing reaches your live shop until your developer merges or applies the changes and deploys the theme.

Last updated:

What does Validiscan change in a Magento theme?

Validiscan changes only the files of your Magento 2 frontend theme, the folder that holds its theme.xml, usually under app/design/frontend. A change to any other file fails validation and is never delivered, and no fix installs an extension or adds a script that rewrites your pages in the visitor’s browser.

Templates and styles that your theme does not override come from its parent theme, such as Luma or Blank, or from Magento’s modules. The recipes do not copy those files into your theme: where a failure comes from a file your theme does not override, the recipe makes no change and the finding is left to a person. Content and settings that you keep in the Magento Admin, such as CMS pages and blocks, Page Builder content, the locale of a store view or the HTML Head settings, are not files of the theme, so the fixes do not change them. Third-party embeds such as payment or chat widgets are not included in a project.

Details: the folders, what a change looks like, and where the recipes add their styles

Validiscan changes only the files of your Magento 2 frontend theme, the folder that holds its theme.xml, usually under app/design/frontend: the .phtml and .html templates, the layout XML, the LESS sources and compiled stylesheets under web/css, the scripts under web/js and the German translation file in i18n. A change to any other file, such as Magento’s own code, an extension or build output, fails validation, and 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. New style rules that a fix adds sit in a block at the end of the stylesheet, between comments that name the recipe, for example /* validiscan:focus-visible-styles */. Smaller changes, such as a corrected colour value or a removed outline: none, are made where the code already is. Words that a fix adds to a .phtml template go through Magento’s translation function __() and are escaped like the theme’s own text; in HTML files that the theme keeps under Magento_Cms/content, the recipes use Magento’s trans directive instead. Magento keeps CMS pages and blocks in the Magento Admin, so a change to one of these files reaches your shop only where your project loads that file into its CMS pages or blocks. No fix installs an extension or adds a script that rewrites your pages in the visitor’s browser, as an overlay does.

The recipes for the focus outline, the tap targets, the skip link and new buttons add their style rules at the end of the theme’s main stylesheet: its _extend.less or _theme.less under web/css/source, with a new _extend.less where a theme built on Luma or Blank has neither, and also a compiled web/css/styles-m.css where the theme ships one. On a theme that is not built on Luma or Blank and has neither LESS sources there nor that compiled file, the recipes for the focus outline, the tap targets and the buttons make no change, and the skip link recipe adds the link without styles.

How does Validiscan get access to your Magento theme?

Through a GitHub repository, or a ZIP of the theme folder uploaded in the customer portal, because Validiscan prepares every fix on a copy of your Magento theme’s source code. Which one fits depends on where the code lives: a repository gets a pull request, a ZIP gets a patch bundle.

The two ways into a Magento theme
Way in You give Changes come back as
Through your GitHub repository Access to the repository, which can hold the theme alone or your whole Magento project, through Validiscan’s GitHub App or an access token A pull request
From a ZIP of the theme folder Your theme folder under app/design/frontend, 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 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.

The repository can hold the theme alone or your whole Magento project. The recipes find the theme by its theme.xml in app/design/frontend/<Vendor>/<theme>, or at the top of a repository that holds only the theme, next to its registration.php. If the repository holds several frontend themes, a recipe works on the theme in which the failing element was found, and changes nothing when it cannot tell which theme that is. A theme that sits elsewhere in a project, for example as a Composer package under vendor, is outside the paths a fix may change. The changes arrive as a pull request.

From a ZIP of the theme folder

Without a GitHub repository, your developer makes a ZIP of the theme. The customer portal gives this instruction: “Ask your developer for a ZIP of your theme folder under app/design/frontend.” 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?

On Magento, 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 does not change your live shop; delivery as an unpublished theme copy uses Shopify’s theme library and exists only for Shopify.

From delivery to your live shop, in five steps
  1. Validiscan delivers the changes it has reviewed and approved: a pull request on a new branch, or a patch bundle for your developer.
  2. 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.
  3. Your developer merges the pull request or applies the patch bundle.
  4. Your developer compiles the stylesheets and deploys the theme: the LESS changes take effect once the theme’s static files are deployed.
  5. 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/a11y-<date>-<id>, 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 styles

A theme based on Luma or Blank keeps its styles as LESS sources, and Magento compiles them into the CSS it serves. Magento’s static files deployment tool, bin/magento setup:static-content:deploy, compiles the LESS files and publishes the result, as Adobe’s documentation on CSS and LESS preprocessing describes. The LESS changes therefore take effect once your developer deploys the changed theme’s static files. Where the theme also ships a compiled web/css/styles-m.css, Magento serves that file as it is, so the recipes make the same change there and the two files stay in step. Where fixes go in a Magento theme, and what a theme update can undo

What do the recipes change in a Magento theme?

Each of Validiscan’s 20 recipes for Magento 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 from your store’s locale html-lang Where your theme’s override of the root template prints its own lang or none, it prints Magento’s own attributes again, so every page gets a valid lang.

    Magento sets the language of a page from the locale of its store view, for example en for the locale en_US, and prints it through its root template. Where your theme overrides that template and prints its own lang, typically the locale code en_US, which is not a valid language tag, or no lang at all, the template prints Magento’s own attributes again, so every page gets a valid lang. A lang set in layout XML in the locale form, such as de_DE, becomes de-DE. A page whose content is in another language than its store view, such as a German legal page in an English store, is left to you: serve it from a store view in that language, or mark the language in the content itself.

    Rules it serves
    • html-has-lang AUTO checked through vs-lang
    • html-lang-valid AUTO checked through vs-lang
    • vs-lang AUTO
    File types it changes
    .phtml
    fixes the vs-lang finding in root.phtml and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Theme/templates/root.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/templates/root.phtml: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/templates/root.phtml: 1 added, 1 removed
    Line before Line after Change Code
    15 15 ?>
    16 16 <!doctype html>
    17 17 <!-- VALIDISCAN-SEEDED vs-lang: root.phtml prints the Magento locale code (en_US) instead of a BCP 47 language tag -->
    18 Removed <html lang="<?= $block->escapeHtmlAttr($htmlLocale) ?>">
    18 Added <html <?= /* @noEscape */ $htmlAttributes ?>>
    19 19 <head <?= /* @noEscape */ $headAttributes ?>>
    20 20 <?= /* @noEscape */ $requireJs ?>
    21 21 <?= /* @noEscape */ $headContent ?>
  • A title on every page document-title-template Empty titles that your theme sets in layout XML are removed, so Magento’s own page title applies again.

    Magento gives every page a title of its own, such as the name of the product or category, or “Search results for:” and the search term, with the prefix or suffix set for your store. A theme can override it with an empty <title> in layout XML, and the page then has no title. The recipe removes such empty titles, so Magento’s own title applies again. Titles with text are your wording and stay as they are. If no layout file of the theme empties the title, the recipe changes nothing: the title then comes from the page itself and from the HTML Head settings in the Magento Admin.

    Rules it serves
    • document-title AUTO
    File types it changes
    .xml
    removes the blank search results title and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_CatalogSearch/layout/catalogsearch_result_index.xml Changes to app/design/frontend/Validiscan/fixture/Magento_CatalogSearch/layout/catalogsearch_result_index.xml: 0 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_CatalogSearch/layout/catalogsearch_result_index.xml: 0 added, 1 removed
    Line before Line after Change Code
    4 4 <!-- VALIDISCAN-SEEDED document-title: a "hide the SEO title" customisation sets an empty page title,
    5 5 so the rendered <title></title> is empty. The visible h1 is set separately by the page.main.title block.
    6 6 Fixed variant: <title>Search results for: 'jacket'</title>. -->
    7 Removed <title> </title>
    8 7 </head>
    9 8 <body>
    10 9 <referenceBlock name="search_result_list" template="Magento_Catalog::product/list.phtml"/>
  • Headings without skipped levels heading-levels A heading that skips a level moves up with the headings after it in its template, where the level before it can be read; a page title hidden with display: none is hidden visually instead.

    A heading that skips a level, such as an h4 right after an h2, moves up to the next level, and the headings that follow it in the same template at its level or deeper move up with it. Classes stay, so styles that the theme sets by class still apply; your developer checks styles that the theme sets by the heading’s tag. Where the level of the heading before it can be read neither from the scan nor from the same template, the heading is left to a person. Magento prints each page’s title as its h1. A theme that hides the title on a page with display: none removes the page’s only h1, so the recipe hides it visually instead, the way Magento’s own styles hide text, and the h1 stays in the outline that screen readers use. A title removed in layout XML is left to a person, because bringing it back would change the page’s look.

    Rules it serves
    • heading-order AUTO checked through vs-headings-landmarks
    • page-has-heading-one AUTO checked through vs-headings-landmarks
    • vs-headings-landmarks AUTO
    File types it changes
    .css.html.less
    keeps the hidden home page title as the h1 and validates Example data

    app/design/frontend/Validiscan/fixture/web/css/source/_theme.less Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 6 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 6 added, 1 removed
    Line before Line after Change Code
    1093 1093
    1094 1094 // VALIDISCAN-SEEDED vs-headings-landmarks: the home page title is display:none, so the page has no h1.
    1095 1095 .cms-index-index .page-title-wrapper {
    1096 Removed display: none;
    1096 Added position: absolute;
    1097 Added width: 1px;
    1098 Added height: 1px;
    1099 Added overflow: hidden;
    1100 Added clip: rect(0 0 0 0);
    1101 Added white-space: nowrap;
    1097 1102 }
    1098 1103
    1099 1104 // VALIDISCAN-SEEDED color-contrast: #929292 on white is 3.1:1.

    app/design/frontend/Validiscan/fixture/web/css/styles-m.css Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 6 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 6 added, 1 removed
    Line before Line after Change Code
    1070 1070 }
    1071 1071
    1072 1072 .cms-index-index .page-title-wrapper {
    1073 Removed display: none;
    1073 Added position: absolute;
    1074 Added width: 1px;
    1075 Added height: 1px;
    1076 Added overflow: hidden;
    1077 Added clip: rect(0 0 0 0);
    1078 Added white-space: nowrap;
    1074 1079 }
    1075 1080
    1076 1081 .block-promo .info,
    moves the journal headings from h4 to h3 and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html Changes to app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html: 3 added, 3 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html: 3 added, 3 removed
    Line before Line after Change Code
    152 152 <!-- VALIDISCAN-SEEDED vs-headings-landmarks (h2 followed by h4) + vs-link-purpose ("Read more" links to three different pages) -->
    153 153 <div class="journal-items">
    154 154 <article class="journal-item">
    155 Removed <h4 class="journal-title">Layering for wet autumn days</h4>
    155 Added <h3 class="journal-title">Layering for wet autumn days</h3>
    156 156 <p>How a light shell over a warm mid-layer keeps you dry on the way to work.</p>
    157 157 <a class="journal-link" href="/magento/juno-jacket.html">Read more</a>
    158 158 </article>
    159 159 <article class="journal-item">
    160 Removed <h4 class="journal-title">Caring for technical shells</h4>
    160 Added <h3 class="journal-title">Caring for technical shells</h3>
    161 161 <p>Washing, drying and re-proofing: three steps that keep a rain shell working for years.</p>
    162 162 <a class="journal-link" href="/magento/nadia-elements-shell.html">Read more</a>
    163 163 </article>
    164 164 <article class="journal-item">
    165 Removed <h4 class="journal-title">Running through the dark season</h4>
    165 Added <h3 class="journal-title">Running through the dark season</h3>
    166 166 <p>Reflective details, lights and route planning for early morning runs.</p>
    167 167 <a class="journal-link" href="/magento/ingrid-running-jacket.html">Read more</a>
    168 168 </article>

Keyboard use and focus

  • A visible focus outline focus-visible-styles One outline for keyboard focus, in the theme’s brand colour, adjusted to a contrast of at least 3:1 against the page background.

    The theme’s stylesheet gets one outline for keyboard focus: 2 pixels, solid, 2 pixels away from the element, in the theme’s brand colour. The colour comes from a brand or primary colour variable, else from the primary button, else from the link colour, and is adjusted until it has a contrast of at least 3:1 against the page background. Areas with a background of their own, such as a dark top bar, get an outline colour that has that contrast there. Theme rules that remove the outline from links, buttons and other controls on focus are taken out, unless they show focus in another way, such as a border or a shadow.

    Rules it serves
    • vs-focus-visible AUTO
    File types it changes
    .css.less
    adds the outline, removes the outline: none rule and validates Example data

    app/design/frontend/Validiscan/fixture/web/css/source/_theme.less Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 10 added, 12 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 10 added, 12 removed
    Line before Line after Change Code
    1079 1079 color: #fff;
    1080 1080 }
    1081 1081
    1082 Removed // VALIDISCAN-SEEDED vs-focus-visible: the theme removes the focus ring from links and actions.
    1083 Removed .navigation a:focus,
    1084 Removed .header.links a:focus,
    1085 Removed .product-item-link:focus,
    1086 Removed .footer a:focus,
    1087 Removed .action:focus,
    1088 Removed .logo:focus,
    1089 Removed .block-promo:focus,
    1090 Removed .journal-link:focus {
    1091 Removed outline: none;
    1092 Removed }
    1093 Removed
    1094 1082 // VALIDISCAN-SEEDED vs-headings-landmarks: the home page title is display:none, so the page has no h1.
    1095 1083 .cms-index-index .page-title-wrapper {
    1096 1084 display: none;
    1264 1252 height: 18px;
    1265 1253 stroke-width: 3;
    1266 1254 }
    1255 Added
    1256 Added /* validiscan:focus-visible-styles */
    1257 Added :focus-visible {
    1258 Added outline: 2px solid #1979c3;
    1259 Added outline-offset: 2px;
    1260 Added }
    1261 Added .panel.wrapper :focus-visible {
    1262 Added outline-color: #a1d0f3;
    1263 Added }
    1264 Added /* /validiscan:focus-visible-styles */

    app/design/frontend/Validiscan/fixture/web/css/styles-m.css Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 10 added, 11 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 10 added, 11 removed
    Line before Line after Change Code
    1058 1058 color: #fff;
    1059 1059 }
    1060 1060
    1061 Removed .navigation a:focus,
    1062 Removed .header.links a:focus,
    1063 Removed .product-item-link:focus,
    1064 Removed .footer a:focus,
    1065 Removed .action:focus,
    1066 Removed .logo:focus,
    1067 Removed .block-promo:focus,
    1068 Removed .journal-link:focus {
    1069 Removed outline: none;
    1070 Removed }
    1071 Removed
    1072 1061 .cms-index-index .page-title-wrapper {
    1073 1062 display: none;
    1074 1063 }
    1232 1221 height: 18px;
    1233 1222 stroke-width: 3;
    1234 1223 }
    1224 Added
    1225 Added /* validiscan:focus-visible-styles */
    1226 Added :focus-visible {
    1227 Added outline: 2px solid #1979c3;
    1228 Added outline-offset: 2px;
    1229 Added }
    1230 Added .panel.wrapper :focus-visible {
    1231 Added outline-color: #a1d0f3;
    1232 Added }
    1233 Added /* /validiscan:focus-visible-styles */
  • A tab order that follows the page remove-positive-tabindex Once a reported element carries a tabindex above 0, every fixed positive tabindex in the theme’s templates is removed or set to 0.

    A tabindex above 0 pulls an element to the front of the keyboard order. Such values only work relative to each other across the whole page, so once a reported element is shown to carry one in the theme’s templates, the recipe removes every fixed positive tabindex in the theme’s templates, also on elements the finding did not list. Links, buttons and form fields lose the attribute; other elements get tabindex="0", so they stay reachable while the Tab key follows the order of the page. A tabindex that PHP code computes is left to a person. Validiscan’s check vs-focus-order also reports focus that jumps around the page for other reasons, such as content placed elsewhere by its styles; it is MANUAL, so its findings come with guidance, not with this change.

    Rules it serves
    • tabindex AUTO
    • vs-focus-order MANUAL
    File types it changes
    .phtml
    removes tabindex 1-4 from the login form and the CAPTCHA and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/captcha-math.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/captcha-math.phtml: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/captcha-math.phtml: 1 added, 1 removed
    Line before Line after Change Code
    14 14 <div class="field captcha required" role="group" aria-labelledby="captcha-question" data-captcha-type="math">
    15 15 <label for="captcha_user_login" class="label" id="captcha-question"><span><?= $escaper->escapeHtml(__('Security question: what is %1 + %2?', $a, $b)) ?></span></label>
    16 16 <div class="control captcha">
    17 Removed <input name="captcha[user_login]" type="text" class="input-text required-entry" id="captcha_user_login" autocomplete="off" inputmode="numeric" tabindex="3" data-captcha-answer-hash="<?= $escaper->escapeHtmlAttr(md5((string) ($a + $b))) ?>">
    17 Added <input name="captcha[user_login]" type="text" class="input-text required-entry" id="captcha_user_login" autocomplete="off" inputmode="numeric" data-captcha-answer-hash="<?= $escaper->escapeHtmlAttr(md5((string) ($a + $b))) ?>">
    18 18 </div>
    19 19 </div>

    app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/login.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/login.phtml: 3 added, 3 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/login.phtml: 3 added, 3 removed
    Line before Line after Change Code
    27 27 <div class="field email required">
    28 28 <label class="label" for="email"><span><?= $escaper->escapeHtml(__('Email')) ?></span></label>
    29 29 <div class="control">
    30 Removed <input name="login[username]" value="<?= $escaper->escapeHtmlAttr($username) ?>" autocomplete="off" id="email" type="email" class="input-text mage-error" title="<?= $escaper->escapeHtmlAttr(__('Email')) ?>" aria-invalid="true" tabindex="1" onpaste="return false;" data-mage-init='{"mage/trim-input":{}}'>
    30 Added <input name="login[username]" value="<?= $escaper->escapeHtmlAttr($username) ?>" autocomplete="off" id="email" type="email" class="input-text mage-error" title="<?= $escaper->escapeHtmlAttr(__('Email')) ?>" aria-invalid="true" onpaste="return false;" data-mage-init='{"mage/trim-input":{}}'>
    31 31 <div for="email" generated="true" class="mage-error" id="email-error"><?= $escaper->escapeHtml(__('Please enter a valid email address (Ex: johndoe@domain.com).')) ?></div>
    32 32 </div>
    33 33 </div>
    35 35 <div class="field password required">
    36 36 <label for="pass" class="label"><span><?= $escaper->escapeHtml(__('Password')) ?></span></label>
    37 37 <div class="control">
    38 Removed <input name="login[password]" type="password" autocomplete="off" class="input-text" id="pass" title="<?= $escaper->escapeHtmlAttr(__('Password')) ?>" tabindex="2" onpaste="return false;" oncopy="return false;">
    38 Added <input name="login[password]" type="password" autocomplete="off" class="input-text" id="pass" title="<?= $escaper->escapeHtmlAttr(__('Password')) ?>" onpaste="return false;" oncopy="return false;">
    39 39 </div>
    40 40 </div>
    41 41 <!-- VALIDISCAN-SEEDED vs-accessible-auth: arithmetic CAPTCHA with no alternative -->
    42 42 <?= $block->getChildHtml('form_additional_info') /* math CAPTCHA module: div.field.captcha, input#captcha_user_login tabindex="3" */ ?>
    43 43 <div class="actions-toolbar">
    44 Removed <div class="primary"><button type="submit" class="action login primary" name="send" id="send2" tabindex="4"><span><?= $escaper->escapeHtml(__('Sign In')) ?></span></button></div>
    44 Added <div class="primary"><button type="submit" class="action login primary" name="send" id="send2"><span><?= $escaper->escapeHtml(__('Sign In')) ?></span></button></div>
    45 45 <div class="secondary"><a class="action remind" href="<?= $escaper->escapeUrl($block->getUrl('contact')) ?>"><span><?= $escaper->escapeHtml(__('Forgot Your Password? Contact us')) ?></span></a></div>
    46 46 </div>
    47 47 </fieldset>

Zoom and tap targets

  • Zoom allowed on phones meta-viewport-zoom The theme’s viewport tag loses user-scalable=no and any maximum-scale below 2, so visitors can zoom the page on a phone; a value that PHP code computes is listed for your developer.

    The recipe removes user-scalable=no and any maximum-scale below 2 from the viewport tag in the theme’s layout XML, such as default_head_blocks.xml, where Luma declares it, or in its templates, so visitors can zoom the page on a phone. For the best-practice rule meta-viewport-large the limit is 5. The other viewport settings stay as they were. A viewport value that PHP code computes is listed for your developer to check. If the tag that blocks zoom does not come from the theme, for example from the HTML Head settings in the Magento Admin or from a module, the recipe changes nothing.

    Rules it serves
    • meta-viewport AUTO
    • meta-viewport-large AUTO
    File types it changes
    .xml
    removes the zoom block from default_head_blocks.xml and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Theme/layout/default_head_blocks.xml Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/layout/default_head_blocks.xml: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/layout/default_head_blocks.xml: 1 added, 1 removed
    Line before Line after Change Code
    3 3 <head>
    4 4 <!-- VALIDISCAN-SEEDED meta-viewport: pinch zoom disabled (Luma 2.0-2.3 default kept by the theme).
    5 5 Fixed variant: content="width=device-width, initial-scale=1". -->
    6 Removed <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no"/>
    6 Added <meta name="viewport" content="width=device-width, initial-scale=1"/>
    7 7 <meta name="format-detection" content="telephone=no"/>
    8 8 <css src="css/styles-m.css"/>
    9 9 <css src="css/styles-l.css" media="screen and (min-width: 768px)"/>
  • Larger tap targets target-size-padding Buttons and links the scan found too small get a minimum size of 24 by 24 pixels, through classes the theme’s stylesheet can address.

    Buttons and links that the scan found too small, such as social media icons or quantity buttons, get a minimum size of 24 by 24 pixels in a block at the end of the theme’s stylesheet. The block reuses the selectors with which the theme made them small, or their own classes, and sets a minimum width and height, which wins over a smaller width or height. The icons inside and the layout around them stay as they are. A link or other inline element without a display rule becomes inline-flex, because inline elements ignore a minimum size. A control without a class the stylesheet can address is left to a person.

    Rules it serves
    • vs-target-size AUTO
    File types it changes
    .css.less
    sizes the three flagged controls and validates Example data

    app/design/frontend/Validiscan/fixture/web/css/source/_theme.less Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 9 added, 0 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 9 added, 0 removed
    Line before Line after Change Code
    1264 1264 height: 18px;
    1265 1265 stroke-width: 3;
    1266 1266 }
    1267 Added
    1268 Added /* validiscan:target-size-padding */
    1269 Added .social-link,
    1270 Added .actions-secondary .action,
    1271 Added .qty-step {
    1272 Added min-width: 24px;
    1273 Added min-height: 24px;
    1274 Added }
    1275 Added /* /validiscan:target-size-padding */

    app/design/frontend/Validiscan/fixture/web/css/styles-m.css Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 9 added, 0 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 9 added, 0 removed
    Line before Line after Change Code
    1232 1232 height: 18px;
    1233 1233 stroke-width: 3;
    1234 1234 }
    1235 Added
    1236 Added /* validiscan:target-size-padding */
    1237 Added .social-link,
    1238 Added .actions-secondary .action,
    1239 Added .qty-step {
    1240 Added min-width: 24px;
    1241 Added min-height: 24px;
    1242 Added }
    1243 Added /* /validiscan:target-size-padding */

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, transitions of 5 seconds or more and smooth scrolling that the theme’s stylesheets start are switched off.

    When a visitor’s device is set to reduce motion, a block at the end of each theme stylesheet switches off the animations, the transitions of 5 seconds or more and the smooth scrolling that this stylesheet starts, selector by selector. Where the theme’s own scripts in web/js start a slider with autoplay switched on, the slider starts without autoplay for those visitors. A rule nested inside another LESS rule is listed for your developer. Animations from Magento’s own code or from extensions are not in the theme’s files and need their own guard.

    Rules it serves
    • vs-motion AUTO
    File types it changes
    .css.less
    guards the badge and slider animations and validates Example data

    app/design/frontend/Validiscan/fixture/web/css/source/_theme.less Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 9 added, 0 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 9 added, 0 removed
    Line before Line after Change Code
    1264 1264 height: 18px;
    1265 1265 stroke-width: 3;
    1266 1266 }
    1267 Added
    1268 Added /* validiscan:reduced-motion-guard */
    1269 Added @media (prefers-reduced-motion: reduce) {
    1270 Added .product-label,
    1271 Added .slick-track {
    1272 Added animation: none;
    1273 Added }
    1274 Added }
    1275 Added /* /validiscan:reduced-motion-guard */

    app/design/frontend/Validiscan/fixture/web/css/styles-m.css Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 9 added, 0 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 9 added, 0 removed
    Line before Line after Change Code
    1232 1232 height: 18px;
    1233 1233 stroke-width: 3;
    1234 1234 }
    1235 Added
    1236 Added /* validiscan:reduced-motion-guard */
    1237 Added @media (prefers-reduced-motion: reduce) {
    1238 Added .product-label,
    1239 Added .slick-track {
    1240 Added animation: none;
    1241 Added }
    1242 Added }
    1243 Added /* /validiscan:reduced-motion-guard */
  • Slideshows that move only when the visitor asks slider-pause-control No pause button on Magento: autoplay is switched off on the Page Builder sliders in the theme’s files, so they change only when the visitor uses the dots or arrows.

    On Magento this recipe adds no pause button. The Page Builder slider runs on Magento’s own scripts, and a theme cannot give it a pause button without overriding Magento’s code. The recipe uses the slider’s own setting instead: on every autoplaying Page Builder slider in the theme’s files, autoplay is switched off, as the Autoplay switch in Page Builder does, and arrows are switched on where the slider would otherwise have no way to move between slides. Endless CSS animations that move a slider by themselves are removed. The slides then change only when the visitor uses the dots or arrows: nothing moves on its own, so there is nothing to pause. A slider stored in the Magento database is not a file of the theme: you switch off its autoplay with the Autoplay setting in Page Builder. A slider that the theme’s own scripts start with autoplay, such as a Slick or Swiper slider, needs a pause button wired into that script, which is left to a person.

    Rules it serves
    • vs-carousel AUTO
    File types it changes
    .css.html.less
    turns off the Page Builder autoplay and the track animation, and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html Changes to app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html: 1 added, 1 removed
    Line before Line after Change Code
    3 3 class="pagebuilder-slider slick-slider"
    4 4 data-content-type="slider"
    5 5 data-appearance="default"
    6 Removed data-autoplay="true"
    6 Added data-autoplay="false"
    7 7 data-autoplay-speed="3000"
    8 8 data-fade="false"
    9 9 data-infinite-loop="true"

    app/design/frontend/Validiscan/fixture/web/css/source/_theme.less Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 0 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 0 added, 1 removed
    Line before Line after Change Code
    1176 1176 .slick-track {
    1177 1177 display: flex;
    1178 1178 width: 300%;
    1179 Removed animation: nw-slider-autoplay 9s ease-in-out infinite;
    1180 1179 }
    1181 1180 .slick-slide {
    1182 1181 flex: 0 0 33.3333%;

    app/design/frontend/Validiscan/fixture/web/css/styles-m.css Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 0 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 0 added, 1 removed
    Line before Line after Change Code
    1147 1147 .slick-track {
    1148 1148 display: flex;
    1149 1149 width: 300%;
    1150 Removed animation: nw-slider-autoplay 9s ease-in-out infinite;
    1151 1150 }
    1152 1151 .slick-slide {
    1153 1152 flex: 0 0 33.3333%;
  • No sound that starts by itself mute-autoplay Video in the theme’s templates that plays on its own is muted and audio no longer starts by itself, unless PHP code switches the autoplay on; on Magento the recipe is not used for vs-media findings.

    A video in the theme’s templates that plays on its own gets muted and playsinline, so it keeps playing silently and inside the page on phones. Audio no longer starts by itself. Embedded YouTube and Vimeo players that start on their own are set to start muted. Autoplay that PHP code switches on is left to a person. In a Validiscan scan, sound that starts by itself is reported by Validiscan’s media check vs-media, which takes the place of the axe-core rule this recipe serves. vs-media, which also covers captions and transcripts, is MANUAL, so on Magento such a finding comes with guidance rather than with this change.

    Rules it serves
    • no-autoplay-audio AUTO checked through vs-media
    File types it changes
    .phtml
    mutes the autoplaying product video and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/view/details.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/view/details.phtml: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/view/details.phtml: 1 added, 1 removed
    Line before Line after Change Code
    44 44 <h3 class="data item title"><?= $escaper->escapeHtml(__('Product video')) ?></h3>
    45 45 <!-- VALIDISCAN-SEEDED vs-media: product video without a captions track -->
    46 46 <?php /* Fixed variant: <track kind="captions" src="...juno-jacket-care.en.vtt" srclang="en" label="English" default>. */ ?>
    47 Removed <video class="product-video-player" autoplay loop controls preload="none" width="560" height="315" poster="<?= $escaper->escapeUrl($product->getData('care_video_poster')) ?>">
    47 Added <video class="product-video-player" autoplay loop controls preload="none" width="560" height="315" poster="<?= $escaper->escapeUrl($product->getData('care_video_poster')) ?>" muted playsinline>
    48 48 <source src="<?= $escaper->escapeUrl($videoUrl) ?>" type="video/mp4">
    49 49 </video>
    50 50 <p><?= $escaper->escapeHtml($product->getData('care_video_caption')) ?></p>

Forms, the cart and embedded content

  • Labels connected to their fields label-association Label text already on the page before a field gets connected to it, as a <label> or through aria-labelledby.

    A form field whose label text is already on the page, before the field, gets connected to that text. A <label> without a for gets one that points at the field. A label text in a <span> or <strong>, such as a “Sort By” text with Luma’s sorter-label class next to the sort menu of a category page, becomes a <label> with the same classes. A label text in a block element, such as <div class="label">, keeps its tag and is connected through aria-labelledby. The text must sit in a container with no other field, so it cannot belong to another one. 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
    • label AUTO checked through vs-forms-ecommerce
    • select-name ASSISTED
    • vs-forms-ecommerce AUTO
    File types it changes
    .phtml
    turns the "Sort By" text into the sorter label and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/list/toolbar.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/list/toolbar.phtml: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/list/toolbar.phtml: 1 added, 1 removed
    Line before Line after Change Code
    12 12 <div class="toolbar-sorter sorter">
    13 13 <!-- VALIDISCAN-SEEDED select-name + vs-forms-ecommerce: the "Sort By" text is a span, not a label for the select -->
    14 14 <?php /* Fixed variant: <label class="sorter-label" for="sorter">Sort By</label>. */ ?>
    15 Removed <span class="sorter-label"><?= $escaper->escapeHtml(__('Sort By')) ?></span>
    15 Added <label class="sorter-label" for="sorter"><?= $escaper->escapeHtml(__('Sort By')) ?></label>
    16 16 <select id="sorter" data-role="sorter" class="sorter-options">
    17 17 <?php foreach ($block->getAvailableOrders() as $key => $order): ?>
    18 18 <option value="<?= $escaper->escapeHtmlAttr($key) ?>"<?= $block->isOrderCurrent($key) ? ' selected' : '' ?>><?= $escaper->escapeHtml(__($order)) ?></option>
  • Account, address and contact fields that browsers can fill in autocomplete-tokens Fields in the theme’s templates that ask for the customer’s own data, in address, account, login, contact and newsletter forms, get the autocomplete value for what they ask for, unless template code writes it.

    Fields in the theme’s templates that ask for the customer’s own data, in address, account, login, contact and newsletter forms, get the autocomplete value for what they ask for, such as email, name, phone number, street address, postal code or password. The value is worked out from the field’s type, name, id, label, placeholder and title, by the same rules as Validiscan’s form check, and billing and shipping fields get their section, as in shipping given-name. Once one field of a form is reported, every field of that form is handled. Passwords get new-password in registration and reset forms and in forms with more than one password field, and current-password elsewhere. Coupon, search, gift recipient, comment, quantity and CAPTCHA fields never get a value. A value that template code writes, or a valid value that looks wrong for the field, is left to a person.

    Rules it serves
    • autocomplete-valid AUTO checked through vs-forms-ecommerce
    • vs-forms-ecommerce AUTO
    File types it changes
    .phtml
    adds tokens to the newsletter, login and contact forms and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Contact/templates/form.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Contact/templates/form.phtml: 3 added, 3 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Contact/templates/form.phtml: 3 added, 3 removed
    Line before Line after Change Code
    21 21 <!-- VALIDISCAN-SEEDED vs-forms-ecommerce: name, email and phone lack autocomplete; required only via the CSS asterisk -->
    22 22 <div class="field name required">
    23 23 <label class="label" for="name"><span><?= $escaper->escapeHtml(__('Name')) ?></span></label>
    24 Removed <div class="control"><input name="name" id="name" title="<?= $escaper->escapeHtmlAttr(__('Name')) ?>" value="<?= $escaper->escapeHtmlAttr($viewModel->getUserName()) ?>" class="input-text" type="text"></div>
    24 Added <div class="control"><input name="name" id="name" title="<?= $escaper->escapeHtmlAttr(__('Name')) ?>" value="<?= $escaper->escapeHtmlAttr($viewModel->getUserName()) ?>" class="input-text" type="text" autocomplete="name"></div>
    25 25 </div>
    26 26 <div class="field email required">
    27 27 <label class="label" for="email"><span><?= $escaper->escapeHtml(__('Email')) ?></span></label>
    28 Removed <div class="control"><input name="email" id="email" title="<?= $escaper->escapeHtmlAttr(__('Email')) ?>" value="<?= $escaper->escapeHtmlAttr($viewModel->getUserEmail()) ?>" class="input-text" type="email"></div>
    28 Added <div class="control"><input name="email" id="email" title="<?= $escaper->escapeHtmlAttr(__('Email')) ?>" value="<?= $escaper->escapeHtmlAttr($viewModel->getUserEmail()) ?>" class="input-text" type="email" autocomplete="email"></div>
    29 29 </div>
    30 30 <div class="field telephone">
    31 31 <label class="label" for="telephone"><span><?= $escaper->escapeHtml(__('Phone Number')) ?></span></label>
    32 Removed <div class="control"><input name="telephone" id="telephone" title="<?= $escaper->escapeHtmlAttr(__('Phone Number')) ?>" value="<?= $escaper->escapeHtmlAttr($viewModel->getUserTelephone()) ?>" class="input-text" type="tel"></div>
    32 Added <div class="control"><input name="telephone" id="telephone" title="<?= $escaper->escapeHtmlAttr(__('Phone Number')) ?>" value="<?= $escaper->escapeHtmlAttr($viewModel->getUserTelephone()) ?>" class="input-text" type="tel" autocomplete="tel"></div>
    33 33 </div>
    34 34 <div class="field comment required">
    35 35 <label class="label" for="comment"><span><?= $escaper->escapeHtml(__('What’s on your mind?')) ?></span></label>

    app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/login.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/login.phtml: 2 added, 2 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/login.phtml: 2 added, 2 removed
    Line before Line after Change Code
    27 27 <div class="field email required">
    28 28 <label class="label" for="email"><span><?= $escaper->escapeHtml(__('Email')) ?></span></label>
    29 29 <div class="control">
    30 Removed <input name="login[username]" value="<?= $escaper->escapeHtmlAttr($username) ?>" autocomplete="off" id="email" type="email" class="input-text mage-error" title="<?= $escaper->escapeHtmlAttr(__('Email')) ?>" aria-invalid="true" tabindex="1" onpaste="return false;" data-mage-init='{"mage/trim-input":{}}'>
    30 Added <input name="login[username]" value="<?= $escaper->escapeHtmlAttr($username) ?>" autocomplete="email" id="email" type="email" class="input-text mage-error" title="<?= $escaper->escapeHtmlAttr(__('Email')) ?>" aria-invalid="true" tabindex="1" onpaste="return false;" data-mage-init='{"mage/trim-input":{}}'>
    31 31 <div for="email" generated="true" class="mage-error" id="email-error"><?= $escaper->escapeHtml(__('Please enter a valid email address (Ex: johndoe@domain.com).')) ?></div>
    32 32 </div>
    33 33 </div>
    35 35 <div class="field password required">
    36 36 <label for="pass" class="label"><span><?= $escaper->escapeHtml(__('Password')) ?></span></label>
    37 37 <div class="control">
    38 Removed <input name="login[password]" type="password" autocomplete="off" class="input-text" id="pass" title="<?= $escaper->escapeHtmlAttr(__('Password')) ?>" tabindex="2" onpaste="return false;" oncopy="return false;">
    38 Added <input name="login[password]" type="password" autocomplete="current-password" class="input-text" id="pass" title="<?= $escaper->escapeHtmlAttr(__('Password')) ?>" tabindex="2" onpaste="return false;" oncopy="return false;">
    39 39 </div>
    40 40 </div>
    41 41 <!-- VALIDISCAN-SEEDED vs-accessible-auth: arithmetic CAPTCHA with no alternative -->

    app/design/frontend/Validiscan/fixture/Magento_Newsletter/templates/subscribe.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Newsletter/templates/subscribe.phtml: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Newsletter/templates/subscribe.phtml: 1 added, 1 removed
    Line before Line after Change Code
    18 18 <!-- VALIDISCAN-SEEDED vs-forms-ecommerce (label): newsletter email only has a placeholder, no label and no autocomplete -->
    19 19 <div class="field newsletter">
    20 20 <div class="control">
    21 Removed <input name="email" type="email" id="newsletter" placeholder="<?= $escaper->escapeHtmlAttr(__('Enter your email address')) ?>" data-mage-init='{"mage/trim-input":{}}' data-validate="{required:true, 'validate-email':true}">
    21 Added <input name="email" type="email" id="newsletter" placeholder="<?= $escaper->escapeHtmlAttr(__('Enter your email address')) ?>" data-mage-init='{"mage/trim-input":{}}' data-validate="{required:true, 'validate-email':true}" autocomplete="email">
    22 22 </div>
    23 23 </div>
    24 24 <div class="actions">
  • Price, stock and cart changes announced live-region-cart-variant The containers in the theme’s templates that Magento’s scripts rewrite, such as the price box and the mini cart counter, become live regions; the scripts are not changed.

    Magento’s own scripts rewrite the text inside fixed containers when a customer picks a size or colour or adds a product to the cart. The recipe turns those containers in the theme’s templates into live regions, so screen readers read the new value without the customer having to move there. The price box and the mini cart counter are read in full when they change; the stock message and the page messages, such as the note that a product was added to the cart, get role="status". The scripts themselves are not changed. Where the theme marks the whole page as a live region, which makes screen readers read out every change, that is removed. Where an extension’s script adds it while the page runs, it is left to a person.

    Rules it serves
    • vs-live-updates AUTO
    File types it changes
    .phtml
    makes the price box and the stock message live regions and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/price/final_price.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/price/final_price.phtml: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/price/final_price.phtml: 1 added, 1 removed
    Line before Line after Change Code
    13 13 // Fixed variant: the product-page box carries aria-live="polite" aria-atomic="true" and the stock
    14 14 // message (product/view/type/default.phtml) is role="status".
    15 15 ?>
    16 Removed <div class="price-box price-final_price" data-role="priceBox" data-product-id="<?= (int) $product->getId() ?>" data-price-box="product-id-<?= (int) $product->getId() ?>"<?php if ($isProductPage): ?> data-base-price="<?= (float) $amount ?>"<?php endif; ?>>
    16 Added <div class="price-box price-final_price" data-role="priceBox" data-product-id="<?= (int) $product->getId() ?>" data-price-box="product-id-<?= (int) $product->getId() ?>"<?php if ($isProductPage): ?> data-base-price="<?= (float) $amount ?>"<?php endif; ?> aria-live="polite" aria-atomic="true">
    17 17 <span class="normal-price"><span class="price-container price-final_price tax weee"><span id="product-price-<?= (int) $product->getId() ?>" data-price-amount="<?= (float) $amount ?>" data-price-type="finalPrice" class="price-wrapper"><span class="price"><?= $escaper->escapeHtml($block->renderAmount($block->getPriceType('final_price')->getAmount(), ['display_label' => false])) ?></span></span></span></span>
    18 18 </div>

    app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/view/type/default.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/view/type/default.phtml: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/view/type/default.phtml: 1 added, 1 removed
    Line before Line after Change Code
    11 11 ?>
    12 12 <div class="product-info-stock-sku">
    13 13 <?php if ($block->displayProductStockStatus()): ?>
    14 Removed <div class="stock available" title="<?= $escaper->escapeHtmlAttr(__('Availability')) ?>"><span><?= $escaper->escapeHtml(__('In stock')) ?></span></div>
    14 Added <div class="stock available" title="<?= $escaper->escapeHtmlAttr(__('Availability')) ?>" role="status"><span><?= $escaper->escapeHtml(__('In stock')) ?></span></div>
    15 15 <?php endif; ?>
    16 16 <div class="product attribute sku"><strong class="type"><?= $escaper->escapeHtml(__('SKU')) ?></strong> <div class="value" itemprop="sku"><?= $escaper->escapeHtml($product->getSku()) ?></div></div>
    17 17 </div>
  • Titles for embedded maps and videos iframe-title An embedded frame without a title gets a translatable title that says what it shows, where the code reveals it.

    An embedded frame without a title, such as a map, a YouTube or Vimeo video, a calendar, a form or a reviews widget, gets a translatable title that says what it shows. The recipe reads what the frame shows from its address, including the fixed parts of an address that PHP code builds, and from its classes, id and name. A frame whose content the code does not reveal gets no title from the recipe and is left to a person.

    Rules it serves
    • frame-title AUTO
    File types it changes
    .phtml
    titles the store map on the contact page and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Contact/templates/form.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Contact/templates/form.phtml: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Contact/templates/form.phtml: 1 added, 1 removed
    Line before Line after Change Code
    48 48 <p>Nordwind Outfitters (fixture)<br>Musterstraße 12<br>10115 Berlin, Germany</p>
    49 49 <p><?= $escaper->escapeHtml(__('Monday to Saturday, 10:00 to 19:00')) ?></p>
    50 50 <!-- VALIDISCAN-SEEDED frame-title: the store map iframe has no title -->
    51 Removed <iframe src="<?= $escaper->escapeUrl($block->getBaseUrl() . 'media/wysiwyg/store-map.html') ?>" width="480" height="260" loading="lazy"></iframe>
    51 Added <iframe src="<?= $escaper->escapeUrl($block->getBaseUrl() . 'media/wysiwyg/store-map.html') ?>" width="480" height="260" loading="lazy" title="<?= $escaper->escapeHtmlAttr(__('Store location map')) ?>"></iframe>
    52 52 </div>
    53 53 </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. In a project, button-semantics is applied to findings of the slideshow check vs-carousel, which is AUTO: the fix engine tries it when the slider recipe above does not fix such a finding. On Magento the other three serve only ASSISTED rules: a finding of those rules takes the ASSISTED way, and these three recipes are not applied to it.

  • Text colours with enough contrast contrast-tokens A failing text colour defined in exactly one place moves to the nearest colour of its hue that passes on every measured background; 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 colour of the same hue that reaches the required contrast against every background the scan measured for it. One place can be a LESS variable or CSS custom property used only for text colour, or a single color rule. Where the theme ships a compiled styles-m.css, it gets the same change. A colour that is used in several places, also as a background or border, or in rules whose background the scan did not measure, is left to a person.

    Rules it serves
    • color-contrast ASSISTED
    File types it changes
    .css.less
    darkens the single #929292 token and validates Example data

    app/design/frontend/Validiscan/fixture/web/css/source/_theme.less Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 1 added, 1 removed
    Line before Line after Change Code
    1099 1099 // VALIDISCAN-SEEDED color-contrast: #929292 on white is 3.1:1.
    1100 1100 .block-promo .info,
    1101 1101 .toolbar-amount {
    1102 Removed color: #929292;
    1102 Added color: #767676;
    1103 1103 }
    1104 1104
    1105 1105 // VALIDISCAN-SEEDED vs-reflow: the promo row keeps a 1100px minimum width at every breakpoint.

    app/design/frontend/Validiscan/fixture/web/css/styles-m.css Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 1 added, 1 removed
    Line before Line after Change Code
    1075 1075
    1076 1076 .block-promo .info,
    1077 1077 .toolbar-amount {
    1078 Removed color: #929292;
    1078 Added color: #767676;
    1079 1079 }
    1080 1080
    1081 1081 .block-promo-wrapper {
  • Decorative images hidden from screen readers decorative-image-alt Only images the template proves decorative get an empty alt or aria-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 already has text or a name, such as a promotion tile with its own heading; it is an icon, at most 48 pixels in size or with an icon class or file name, next to its own text label; or it is a spacer or divider. An <img> then gets an empty alt, and an SVG icon gets aria-hidden="true", so screen readers skip it instead of reading out a file name. Every other image is treated as content, and its text alternative needs words a person chooses.

    Rules it serves
    • image-alt ASSISTED
    • role-img-alt ASSISTED
    • svg-img-alt ASSISTED
    File types it changes
    .html
    marks the promo tile image decorative and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html Changes to app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html: 1 added, 1 removed
    Line before Line after Change Code
    111 111 <div class="block-promo-wrapper block-promo-hp">
    112 112 <!-- VALIDISCAN-SEEDED image-alt: promo image without an alt attribute -->
    113 113 <a class="block-promo home-main" href="/magento/women/tops-women/jackets-women.html">
    114 Removed <img src="/magento/media/wysiwyg/home/home-main.svg" width="400" height="240" />
    114 Added <img src="/magento/media/wysiwyg/home/home-main.svg" width="400" height="240" alt="" />
    115 115 <span class="content"
    116 116 ><!-- VALIDISCAN-SEEDED color-contrast: .info text is #929292 on white (3.1:1) --><span
    117 117 class="info"
  • Mouse-only controls turned into buttons button-semantics Clickable <div> and <span> elements in sliders and cookie banners become real buttons where the code shows they are controls; in a project the recipe is applied to vs-carousel findings.

    Clickable <div> and <span> elements in sliders and cookie banners become real buttons. The recipe counts an element as a control only when the code says so: it has role="button", a click handler or a Knockout click binding, or a class or data attribute that the theme’s script selects to handle clicks. Classes, data attributes and ARIA attributes stay, so the theme’s styles and scripts find the control as before, and Enter and Space now do what a click does. A small style reset at the end of the theme’s stylesheet keeps its look, also on hover and focus, and focus outlines are not touched. A control that holds blocks, links or fields, or that has no class, is left to a person.

    Rules it serves
    • vs-carousel AUTO
    • vs-cookie-banner ASSISTED
    File types it changes
    .css.less.phtml
    turns span controls of the cookie bar into buttons and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Cookie/templates/html/notices.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Cookie/templates/html/notices.phtml: 2 added, 2 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Cookie/templates/html/notices.phtml: 2 added, 2 removed
    Line before Line after Change Code
    24 24 <div class="amgdprcookie-bar-block">
    25 25 <p class="amgdprcookie-policy"><?= $escaper->escapeHtml(__('We use cookies to give you the best shopping experience and to analyse our traffic. By continuing to browse you agree to our')) ?> <a href="<?= $escaper->escapeUrl($block->getPrivacyPolicyLink()) ?>" class="amgdprcookie-link"><?= $escaper->escapeHtml(__('cookie policy')) ?></a>.</p>
    26 26 <div class="amgdprcookie-buttons-block">
    27 Removed <span class="amgdprcookie-button -allow" data-amcookie-js="allow" role="button" tabindex="0"><svg class="icon" viewBox="0 0 24 24"><path d="M4 12l5 5L20 6"/></svg></span>
    28 Removed <span class="amgdprcookie-button -decline" data-amcookie-js="decline" role="button" tabindex="0"><svg class="icon" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></span>
    27 Added <button class="amgdprcookie-button -allow" data-amcookie-js="allow" type="button"><svg class="icon" viewBox="0 0 24 24"><path d="M4 12l5 5L20 6"/></svg></button>
    28 Added <button class="amgdprcookie-button -decline" data-amcookie-js="decline" type="button"><svg class="icon" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
    29 29 </div>
    30 30 </div>
    31 31 </div>

    app/design/frontend/Validiscan/fixture/web/css/source/_theme.less Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 30 added, 0 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/source/_theme.less: 30 added, 0 removed
    Line before Line after Change Code
    1264 1264 height: 18px;
    1265 1265 stroke-width: 3;
    1266 1266 }
    1267 Added
    1268 Added /* validiscan:button-semantics */
    1269 Added button.amgdprcookie-button.-allow,
    1270 Added button.amgdprcookie-button.-decline {
    1271 Added -webkit-appearance: none;
    1272 Added appearance: none;
    1273 Added margin: 0;
    1274 Added padding: 0;
    1275 Added box-shadow: none;
    1276 Added font: inherit;
    1277 Added letter-spacing: inherit;
    1278 Added text-align: inherit;
    1279 Added text-transform: inherit;
    1280 Added text-shadow: none;
    1281 Added }
    1282 Added button.amgdprcookie-button.-allow:hover,
    1283 Added button.amgdprcookie-button.-allow:focus,
    1284 Added button.amgdprcookie-button.-allow:active {
    1285 Added background: @nw-primary;
    1286 Added border: 0;
    1287 Added color: #fff;
    1288 Added }
    1289 Added button.amgdprcookie-button.-decline:hover,
    1290 Added button.amgdprcookie-button.-decline:focus,
    1291 Added button.amgdprcookie-button.-decline:active {
    1292 Added background: #fff;
    1293 Added border: 1px solid @nw-border;
    1294 Added color: @nw-text;
    1295 Added }
    1296 Added /* /validiscan:button-semantics */

    app/design/frontend/Validiscan/fixture/web/css/styles-m.css Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 30 added, 0 removed

    Changes to app/design/frontend/Validiscan/fixture/web/css/styles-m.css: 30 added, 0 removed
    Line before Line after Change Code
    1232 1232 height: 18px;
    1233 1233 stroke-width: 3;
    1234 1234 }
    1235 Added
    1236 Added /* validiscan:button-semantics */
    1237 Added button.amgdprcookie-button.-allow,
    1238 Added button.amgdprcookie-button.-decline {
    1239 Added -webkit-appearance: none;
    1240 Added appearance: none;
    1241 Added margin: 0;
    1242 Added padding: 0;
    1243 Added box-shadow: none;
    1244 Added font: inherit;
    1245 Added letter-spacing: inherit;
    1246 Added text-align: inherit;
    1247 Added text-transform: inherit;
    1248 Added text-shadow: none;
    1249 Added }
    1250 Added button.amgdprcookie-button.-allow:hover,
    1251 Added button.amgdprcookie-button.-allow:focus,
    1252 Added button.amgdprcookie-button.-allow:active {
    1253 Added background: #1979c3;
    1254 Added border: 0;
    1255 Added color: #fff;
    1256 Added }
    1257 Added button.amgdprcookie-button.-decline:hover,
    1258 Added button.amgdprcookie-button.-decline:focus,
    1259 Added button.amgdprcookie-button.-decline:active {
    1260 Added background: #fff;
    1261 Added border: 1px solid #c2c2c2;
    1262 Added color: #333;
    1263 Added }
    1264 Added /* /validiscan:button-semantics */
  • Names for icon-only buttons and links icon-accessible-name Icon-only buttons and links whose purpose the code reveals get an aria-label, in the wording of Magento’s Luma theme where Luma has one; not applied in a project.

    Buttons and links that show only an icon, such as search, mini cart, account, wish list, compare, menu, close, previous and next, and the choices of a cookie banner, get an aria-label that says what they do, in the wording of Magento’s Luma theme where Luma has one, such as “Search”, “My Cart” or “Allow Cookies”. The purpose is read from the control’s classes, id, data attributes and link target, from its icon and from the form or cookie banner around it. A control whose purpose the code does not reveal, such as a social media link built in a loop from an address stored as data, or a link whose only content is a product photo, is left to a person, because its name depends on your content.

    Rules it serves
    • button-name ASSISTED
    • link-name ASSISTED
    • vs-cookie-banner ASSISTED
    File types it changes
    .phtml
    names the icon-only search button and validates Example data

    app/design/frontend/Validiscan/fixture/Magento_Search/templates/form.mini.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Search/templates/form.mini.phtml: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Search/templates/form.mini.phtml: 1 added, 1 removed
    Line before Line after Change Code
    20 20 <div class="actions">
    21 21 <!-- VALIDISCAN-SEEDED button-name: icon-only search button without text, title or aria-label -->
    22 22 <?php /* Fixed variant: <button type="submit" title="Search" class="action search"><span class="sr-only">Search</span>(icon, aria-hidden)</button>; the form gets role="search" aria-label="Site search". */ ?>
    23 Removed <button type="submit" class="action search"><svg class="icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></svg></button>
    23 Added <button type="submit" class="action search" aria-label="<?= $escaper->escapeHtmlAttr(__('Search')) ?>"><svg class="icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></svg></button>
    24 24 </div>
    25 25 </form>
    26 26 </div>
    names the minicart link and leaves the product photo link for review Example data

    app/design/frontend/Validiscan/fixture/Magento_Checkout/templates/cart/minicart.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Checkout/templates/cart/minicart.phtml: 1 added, 1 removed

    Changes to app/design/frontend/Validiscan/fixture/Magento_Checkout/templates/cart/minicart.phtml: 1 added, 1 removed
    Line before Line after Change Code
    13 13 ?>
    14 14 <!-- VALIDISCAN-SEEDED link-name + aria-valid-attr-value: the "My Cart" text was removed from the minicart link (only an icon and an empty counter remain) and aria-expanded carries the invalid value "closed" -->
    15 15 <div data-block="minicart" class="minicart-wrapper">
    16 Removed <a class="action showcart" href="<?= $escaper->escapeUrl($block->getShoppingCartUrl()) ?>" data-bind="scope: 'minicart_content'" aria-haspopup="true" aria-expanded="closed">
    16 Added <a class="action showcart" href="<?= $escaper->escapeUrl($block->getShoppingCartUrl()) ?>" data-bind="scope: 'minicart_content'" aria-haspopup="true" aria-expanded="closed" aria-label="<?= $escaper->escapeHtmlAttr(__('My Cart')) ?>">
    17 17 <svg class="icon" viewBox="0 0 24 24"><path d="M3 4h2l2.4 11.2a2 2 0 0 0 2 1.6h7.7a2 2 0 0 0 2-1.5L21 8H6"/><circle cx="10" cy="20" r="1.5"/><circle cx="17" cy="20" r="1.5"/></svg>
    18 18 <span class="counter qty empty"><span class="counter-number"></span></span>
    19 19 </a>
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 Magento 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 Magento. AUTO means a recipe is the rule’s standard fix. ASSISTED means the rule’s findings usually need judgement. MANUAL means the rule needs a person’s decision and comes with guidance. 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 Magento test theme, a child theme of Magento’s Luma theme with accessibility failures put in on purpose. Magento cannot be rendered in the test environment, so no test renders a page: each test compares the change with a stored diff and checks the changed source, for example that it matches the corrected version of the test theme. Your theme’s files will look different.

Before Validiscan reviews a change, the changed files are checked. Each path must lie inside the theme, as described above. .phtml templates get PHP’s own syntax check, which reads the file without running it. Layout XML is checked as XML, stylesheets get a structure check, and HTML templates and JavaScript are parsed.

When a recipe adds words, such as the text of a skip link or the name of a navigation area, it writes them in English, in the wording of Magento’s Luma theme where Luma has one, such as “Skip to Content”. Where your theme has a German translation file in its i18n folder, the recipe adds the German text there, unless the string is already translated. Text for the other languages of your shop is not added.

What stays MANUAL on Magento?

Failures that need a decision about your content or about how a part of the page behaves, which a recipe does not make, such as captions and transcripts for your videos and audio, stay MANUAL on Magento. For a finding of a MANUAL rule, Validiscan gives guidance, not a code change.

The rules below run in scans and have the tier MANUAL on Magento; best-practice rules are not listed.

  • meta-refresh Delayed refresh under 20 hours must not be used
  • nested-interactive Interactive controls must not be nested
  • scrollable-region-focusable Scrollable region must have keyboard access
  • server-side-image-map Server-side image maps must not be used
  • vs-keyboard-trap Keyboard focus must never get trapped
  • vs-focus-order Focus order must follow the visual order
  • vs-dragging Drag-only controls need a single-pointer alternative
  • vs-media Video needs captions, audio needs a transcript, and nothing may autoplay with sound
  • vs-documents Linked documents need a manual accessibility review
Details: what a MANUAL finding gets, the tab order rule, 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 your content or about how a part of the page behaves, which a recipe does not make: captions and transcripts for your videos and audio, a way out of a widget that traps keyboard focus, a second way to use a control that only works by dragging, a focus order that matches the layout, a page that reloads itself after a delay, or the accessibility of linked PDFs and other documents. A forced tab order through a tabindex above 0 is also reported by the axe-core rule tabindex, which the tab order recipe above fixes.

The work on MANUAL findings that need your own content decisions is not part of a project, and neither are PDFs. What automated accessibility testing can and cannot prove

What do you provide?

You provide written permission for a full scan, one way into your theme, your answer on AI drafting of ASSISTED fixes, a developer who reviews the changes, compiles the stylesheets and deploys the theme, some Magento Admin changes, translations of added text for other languages, and your decisions on what stays MANUAL.

  1. Written permission for a full scan of your shop. A full scan puts a product in the cart and opens the first checkout step, without completing a purchase or entering payment data.
  2. One way into your theme: access to the GitHub repository that holds it, or a ZIP of the theme folder under app/design/frontend, uploaded in the customer portal.
  3. Your answer on whether AI may draft fixes of tier ASSISTED. AI drafting is off until you agree.
  4. A developer who reviews the changes, merges the pull request or applies the patch bundle, compiles the stylesheets and deploys the theme.
  5. The changes in the Magento Admin that the fixes do not make: the store view or language markup for content written in another language than its store, page titles and HTML Head settings, and autoplay switched off on Page Builder sliders that are stored in the database.
  6. Translations of the text that fixes add, for the other languages of your shop. The recipes write English, and add German only where your theme already has a German translation file.
  7. 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. A headless storefront, where the pages are built by a separate frontend application instead of a theme, is on request.

Sources

One source, checked for this page on 6 October 2026.

Adobe Commerce developer documentation 1
  1. CSS and LESS preprocessing · Adobe · 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 check

Or 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

Write to Validiscan

For a free check, give the address of your shop and its platform.

Validiscan uses what you send to answer you. Privacy notice