Accessibility fixes in your WooCommerce theme’s code

Validiscan fixes accessibility failures in your WordPress theme or child theme: its PHP templates, including the WooCommerce templates it overrides, and its stylesheets and scripts. Nothing reaches your live shop until you or your developer merge or apply the changes and put the theme live.

Last updated:

What does Validiscan change in a WooCommerce theme?

Validiscan changes your theme’s own files and nothing else: PHP templates, including the WooCommerce templates it overrides, functions.php, stylesheets and scripts. A change to a file other than PHP, CSS, SCSS, JavaScript, HTML or JSON fails Validiscan’s checks, and no fix adds a plugin or an overlay.

The recipes are written for classic WordPress themes, built from PHP templates, not for block themes, whose templates are HTML files. On a block theme, a failure that no recipe fixes is left to a person, and a template changed in the Site Editor is saved in the site’s database, so a change to the theme’s files does not reach it (WordPress Theme Handbook on templates). The recipes change markup only where the theme files Validiscan receives print it. Markup printed from other files, such as those of WordPress, WooCommerce, another plugin or a parent theme that is not part of that source, is left to a person; a cookie banner added by a consent plugin is one example. One exception is WooCommerce’s own billing and shipping fields: a filter in your theme’s functions.php sets their autocomplete values (see the recipe below).

Details: which files a change may touch, the checks on every change, and what a fix may add

Validiscan changes your theme’s own files and nothing else: the PHP templates, including the WooCommerce templates your theme overrides in its woocommerce folder (WooCommerce’s documentation on overriding templates), functions.php, and the theme’s stylesheets and scripts. A change may touch only PHP, CSS, SCSS, JavaScript, HTML and JSON files, and never a file in a top-level vendor, node_modules, wp-admin or wp-includes folder. A change to any other file fails Validiscan’s checks and cannot be approved. Every changed PHP file also goes through PHP’s own syntax check (php -l), which reads the file without running it. The recipes never edit a minified .min.css or .min.js file, which is build output.

Every change is an ordinary edit to a theme file that your developer can read line by line. A few fixes add some lines to the theme’s own script, for example to connect a pause button to a slideshow or to announce a change in the cart. No fix adds a plugin or an overlay, and nothing is injected that corrects your pages in the visitor’s browser. Third-party embeds such as payment or chat widgets are not included in a project.

How does Validiscan get access to your WooCommerce theme?

Validiscan reaches your WooCommerce theme in one of two ways: through the GitHub repository that holds it, or from a ZIP of the theme folder that you upload. Which one fits depends on where your theme’s code lives, and it also decides how the changes come back to you.

The two ways into a WooCommerce theme
Way in You give Changes come back as
Through your GitHub repository Access to the repository that holds your theme or child theme, through Validiscan’s GitHub App or an access token A pull request
From a ZIP of your theme folder Your theme folder under wp-content/themes, with the child theme if you use one, 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 your theme or child 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, which Validiscan keeps encrypted. The App asks for read and write access to the repository’s contents and pull requests, and read access to its metadata. The recipes expect the theme’s style.css and functions.php at the top level of the repository, so the repository should hold the theme folder itself, not a whole WordPress installation. The changes then arrive as a pull request.

From a ZIP of your theme folder

If your theme is not kept in a repository, the customer portal asks you to have your developer make a ZIP of your theme folder under wp-content/themes, including the child theme if you use one. An admin of your organization then uploads it 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 the result to your developer as a patch bundle. Where fixes go in a WooCommerce theme, and what a theme update can undo

How do the changes reach your shop?

Validiscan delivers only changes it has reviewed and approved, as a pull request or as a patch bundle, and neither touches your live WooCommerce shop. Applying the changes and putting them live stays with you or your developer; delivery as an unpublished theme copy exists only on 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 or agency reviews the changes. For a pull request, its checklist asks for a staging or preview environment, not the live shop, and operating the changed elements with the keyboard alone.
  3. You or your developer merge the pull request, or apply the patch bundle and test the changes.
  4. You or your developer put the theme live.
  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 staging or preview environment, not to the live shop, and to operate the changed elements with the keyboard alone. Merging and deploying stay with you.

A patch bundle

From an uploaded theme folder, Validiscan prepares a zip archive for 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 puts the theme live. What a pull request and a patch bundle contain

Is new text added in your shop’s language?

Only in English. Validiscan’s WooCommerce recipes add new text as a translatable string in your theme’s text domain and add no translations. If your shop is in German or another language, translating the new text is your developer’s step.

Translating the new text, in three steps
  1. Regenerate the theme’s .pot file.
  2. Add the translations to its .po files.
  3. Compile the .mo files that WordPress reads.
Details: which text the recipes add, and how WordPress reads translations

Validiscan’s WooCommerce recipes add new text only in English. When a recipe adds text for visitors, such as the words of a skip link, the label of a pause button or the name of an icon button, it adds the text as a translatable string in your theme’s text domain, and it adds no translations. On Shopify, Shopware and Magento the recipes can also add German text; on WooCommerce they do not.

If your shop is in German or another language, translating the new text is your developer’s step: regenerate the theme’s .pot file, add the translations to its .po files and compile the .mo files that WordPress reads, as WordPress’s documentation on localization describes. The recipes below say where they add text.

What do the recipes change in a WooCommerce theme?

Each of Validiscan’s 20 recipes for WooCommerce 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; otherwise the finding is left to a person.

Page structure and navigation

  • Page language from your WordPress settings html-lang A header template whose <html> element leaves out the language, or has a fixed lang that is not valid, gets WordPress’s language_attributes(), which prints the site language.

    The recipe makes the <html> element of your header templates take its language from WordPress’s language_attributes(), which prints the site language, or the language a multilingual plugin sets, so screen readers pronounce the page correctly. A header template that leaves the language out gets the call added, and a fixed lang value that is not valid is replaced by it. If the wrong value comes from the Site Language setting in your WordPress admin, the recipe changes nothing, because that is a setting and not theme code. Where the theme already reads a language field for each page, for example for German legal pages in an English shop, a filter in functions.php declares that page’s language, and the site header and footer keep the shop’s language. Without such a field the recipe does not guess the language.

    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
    .php
    adds language_attributes() to the checkout header of the fixture theme Example data

    header-checkout.php Changes to header-checkout.php: 1 added, 1 removed

    Changes to header-checkout.php: 1 added, 1 removed
    Line before Line after Change Code
    8 8
    9 9 ?><!doctype html>
    10 10 <?php // VALIDISCAN-SEED vs-lang: copied from a landing-page plugin, language_attributes() is missing, so <html> has no lang. ?>
    11 Removed <html>
    11 Added <html <?php language_attributes(); ?>>
    12 12 <head>
    13 13 <meta charset="<?php bloginfo( 'charset' ); ?>">
    14 14 <?php // VALIDISCAN-SEED meta-viewport: zooming is disabled. ?>
  • A title on every page document-title-template What in the theme stops WordPress’s generated page title from appearing is repaired; a title from an SEO plugin or a page’s own settings stays as it is.

    WordPress builds a page title on its own, from the name of the page or product, the page number of a paginated list and the shop name. The recipe repairs what in the theme stops that title from appearing. A title filter that returns a custom title, for example from an SEO field, falls back to the generated title when the field is empty. A fixed <title> text in a header template is replaced by the title WordPress generates. A theme that is not a child theme and prints no title at all gets WordPress’s title support switched on in functions.php. When the title comes from an SEO plugin or from a page’s own settings, the recipe changes nothing.

    Rules it serves
    • document-title AUTO
    File types it changes
    .php
    lets the empty custom SEO title fall back to the generated title Example data

    functions.php Changes to functions.php: 2 added, 1 removed

    Changes to functions.php: 2 added, 1 removed
    Line before Line after Change Code
    87 87 if ( is_singular() && metadata_exists( 'post', get_the_ID(), '_kb_seo_title' ) ) {
    88 88 // VALIDISCAN-SEED document-title: the Contact page has the field saved empty, and an empty
    89 89 // string wins over the generated title, so <title> is empty there.
    90 Removed return (string) get_post_meta( get_the_ID(), '_kb_seo_title', true );
    90 Added $custom_title = trim( (string) get_post_meta( get_the_ID(), '_kb_seo_title', true ) );
    91 Added return '' !== $custom_title ? $custom_title : $title;
    91 92 }
    92 93 return $title;
    93 94 }
  • Headings without skipped levels heading-levels A heading that skips a level gets the next level below the heading before it and keeps its classes, unless the theme’s styles or scripts select it by its level.

    A heading that skips a level, such as an h4 right after an h2, gets the next level below the heading before it, in the template or in the PHP string that prints it. Widget titles and headings in the footer or a sidebar become h2. When the skip comes from a WooCommerce template shown right after the page title, such as Billing details and Shipping details at the checkout, the headings of the same level in the neighbouring templates move with it. Only the level changes: each heading keeps its classes, and a heading that the theme’s styles or scripts select by its level is left as it is. A heading without a class takes the theme’s look for its new level. A page without a main heading gets the element that already looks like its title, with a title class set to at least 24 pixels, turned into the h1. If a page has no such element, the recipe adds no heading and leaves it to a person.

    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
    .php
    gives the home page its <h1> and fixes the teaser and widget title levels Example data

    front-page.php Changes to front-page.php: 2 added, 2 removed

    Changes to front-page.php: 2 added, 2 removed
    Line before Line after Change Code
    24 24
    25 25 <div class="kb-intro">
    26 26 <?php // VALIDISCAN-SEED vs-headings-landmarks: the page title is a styled <p>, the page has no <h1>. ?>
    27 Removed <p class="kb-intro__title"><?php esc_html_e( 'Woollen outdoor clothing, made to last', 'kestrel-birch' ); ?></p>
    27 Added <h1 class="kb-intro__title"><?php esc_html_e( 'Woollen outdoor clothing, made to last', 'kestrel-birch' ); ?></h1>
    28 28 <p><?php echo esc_html( get_the_excerpt() ); ?></p>
    29 29 <p class="kb-popular">
    30 30 <?php esc_html_e( 'Looking for something warm?', 'kestrel-birch' ); ?>
    44 44 <article class="kb-teaser">
    45 45 <?php echo get_the_post_thumbnail( $kb_post, 'woocommerce_single', array( 'loading' => 'lazy' ) ); ?>
    46 46 <?php // VALIDISCAN-SEED vs-headings-landmarks: teaser titles jump from h2 to h4. ?>
    47 Removed <h4 class="kb-teaser__title"><?php echo esc_html( get_the_title( $kb_post ) ); ?></h4>
    47 Added <h3 class="kb-teaser__title"><?php echo esc_html( get_the_title( $kb_post ) ); ?></h3>
    48 48 <p><?php echo esc_html( get_the_excerpt( $kb_post ) ); ?></p>
    49 49 <?php // VALIDISCAN-SEED vs-link-purpose: three identical "Read more" links to different targets. ?>
    50 50 <a class="kb-teaser__more" href="<?php echo esc_url( get_post_meta( $kb_post->ID, '_kb_teaser_url', true ) ); ?>"><?php esc_html_e( 'Read more', 'kestrel-birch' ); ?></a>

    functions.php Changes to functions.php: 2 added, 2 removed

    Changes to functions.php: 2 added, 2 removed
    Line before Line after Change Code
    53 53 'after_widget' => '</div>',
    54 54 // VALIDISCAN-SEED vs-headings-landmarks: footer widget titles are h4, directly after
    55 55 // the h2 sections of the page (skipped heading level).
    56 Removed 'before_title' => '<h4 class="widget-title">',
    57 Removed 'after_title' => '</h4>',
    56 Added 'before_title' => '<h2 class="widget-title">',
    57 Added 'after_title' => '</h2>',
    58 58 )
    59 59 );
    60 60 }
    moves the checkout section headings from h3 to h2, as in the fixed checkout Example data

    woocommerce/checkout/form-billing.php Changes to woocommerce/checkout/form-billing.php: 1 added, 1 removed

    Changes to woocommerce/checkout/form-billing.php: 1 added, 1 removed
    Line before Line after Change Code
    15 15 defined( 'ABSPATH' ) || exit;
    16 16 ?>
    17 17 <div class="woocommerce-billing-fields">
    18 Removed <h3><?php esc_html_e( 'Billing details', 'woocommerce' ); ?></h3>
    18 Added <h2><?php esc_html_e( 'Billing details', 'woocommerce' ); ?></h2>
    19 19
    20 20 <div class="woocommerce-billing-fields__field-wrapper">
    21 21 <?php

    woocommerce/checkout/form-checkout.php Changes to woocommerce/checkout/form-checkout.php: 1 added, 1 removed

    Changes to woocommerce/checkout/form-checkout.php: 1 added, 1 removed
    Line before Line after Change Code
    26 26 </div>
    27 27 </div>
    28 28
    29 Removed <h3 id="order_review_heading"><?php esc_html_e( 'Your order', 'woocommerce' ); ?></h3>
    29 Added <h2 id="order_review_heading"><?php esc_html_e( 'Your order', 'woocommerce' ); ?></h2>
    30 30
    31 31 <div id="order_review" class="woocommerce-checkout-review-order">
    32 32 <?php do_action( 'woocommerce_checkout_order_review' ); ?>

    woocommerce/checkout/form-shipping.php Changes to woocommerce/checkout/form-shipping.php: 2 added, 2 removed

    Changes to woocommerce/checkout/form-shipping.php: 2 added, 2 removed
    Line before Line after Change Code
    16 16 defined( 'ABSPATH' ) || exit;
    17 17 ?>
    18 18 <div class="woocommerce-shipping-fields">
    19 Removed <h3><?php esc_html_e( 'Shipping details', 'kestrel-birch' ); ?></h3>
    19 Added <h2><?php esc_html_e( 'Shipping details', 'kestrel-birch' ); ?></h2>
    20 20 <div class="shipping_address">
    21 21 <div class="woocommerce-shipping-fields__field-wrapper">
    22 22 <?php
    29 29 </div>
    30 30
    31 31 <div class="woocommerce-additional-fields">
    32 Removed <h3><?php esc_html_e( 'Additional information', 'woocommerce' ); ?></h3>
    32 Added <h2><?php esc_html_e( 'Additional information', 'woocommerce' ); ?></h2>
    33 33 <div class="woocommerce-additional-fields__field-wrapper">
    34 34 <?php
    35 35 foreach ( $checkout->get_checkout_fields( 'order' ) as $kb_key => $kb_field ) {

Keyboard use and focus

  • A visible focus outline focus-visible-styles One outline for keyboard focus, in a theme colour or a shade of it with at least 3:1 contrast against the page background.

    The theme’s main stylesheet gets one outline for keyboard focus: 2 pixels, solid, 2 pixels away from the element, in a colour the theme already uses. That is a colour variable whose name says focus, accent, brand, primary or link, or else the colour of links or of the body text. The first of these with a contrast of at least 3:1 against the page background is used; if none has it, the first is made lighter or darker in the same hue until it does. On dark areas with light text, such as a dark top bar, the outline takes the light text colour. An outline: none on focus that leaves nothing in its place is removed from the theme’s rules; rules that hide the outline only for mouse clicks stay. A theme that already has a visible focus outline, and no rule that hides it, is left as it is.

    Rules it serves
    • vs-focus-visible AUTO
    File types it changes
    .css
    replaces the outline reset of the fixture theme with a brand-coloured outline Example data

    assets/css/main.css Changes to assets/css/main.css: 9 added, 3 removed

    Changes to assets/css/main.css: 9 added, 3 removed
    Line before Line after Change Code
    137 137 box-shadow: 0 0 0 2px var(--kb-ink);
    138 138 }
    139 139
    140 Removed /* VALIDISCAN-SEED vs-focus-visible: "remove the ugly focus ring" - nothing replaces it. */
    141 Removed :focus {
    142 Removed outline: none !important;
    140 Added /* validiscan:focus-visible-styles */
    141 Added :focus-visible {
    142 Added outline: 2px solid var(--kb-focus);
    143 Added outline-offset: 2px;
    143 144 }
    144 145
    146 Added .kb-topbar :focus-visible {
    147 Added outline-color: #fff;
    148 Added }
    149 Added /* /validiscan:focus-visible-styles */
    150 Added
    145 151 /* Buttons ------------------------------------------------------------------ */
    146 152
    147 153 .button,
  • A tab order that follows the page remove-positive-tabindex A tabindex above 0 on the template tag of the reported element is removed or set to 0; one that PHP code prints or a script sets is left to a person.

    A tabindex above 0 pulls an element to the front of the keyboard order. The recipe removes it from links, buttons and form fields, which can 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. Only the template tag that renders the reported element is changed. A tabindex that PHP code prints or a script sets is left to a person.

    Rules it serves
    • tabindex AUTO
    • vs-focus-order MANUAL
    File types it changes
    .php
    removes tabindex="1" from the header search field of the fixture theme Example data

    header.php Changes to header.php: 1 added, 1 removed

    Changes to header.php: 1 added, 1 removed
    Line before Line after Change Code
    49 49 <form role="search" method="get" class="woocommerce-product-search" action="<?php echo esc_url( home_url( '/search' ) ); ?>">
    50 50 <label class="screen-reader-text" for="woocommerce-product-search-field-0"><?php esc_html_e( 'Search for:', 'woocommerce' ); ?></label>
    51 51 <?php // VALIDISCAN-SEED vs-focus-order + tabindex: tabindex="1" "so the search is always the first stop". ?>
    52 Removed <input type="search" id="woocommerce-product-search-field-0" class="search-field" placeholder="<?php echo esc_attr__( 'Search products&hellip;', 'woocommerce' ); ?>" value="<?php echo get_search_query(); ?>" name="s" tabindex="1">
    52 Added <input type="search" id="woocommerce-product-search-field-0" class="search-field" placeholder="<?php echo esc_attr__( 'Search products&hellip;', 'woocommerce' ); ?>" value="<?php echo get_search_query(); ?>" name="s">
    53 53 <?php // VALIDISCAN-SEED button-name: icon-only submit button without text. ?>
    54 54 <button type="submit" value="<?php echo esc_attr_x( 'Search', 'submit button', 'woocommerce' ); ?>" class="kb-search-submit"><?php kb_icon( 'search' ); ?></button>
    55 55 <input type="hidden" name="post_type" value="product">
  • Mouse-only controls turned into buttons button-semantics In a cookie banner or slideshow of your theme, clickable elements that only work with a mouse become real buttons, unless the theme styles or scripts them by their tag name.

    Inside a cookie banner or a slideshow that is part of your theme, clickable <span>, <div>, <li> or <i> elements that only work with a mouse become real <button> elements. Every class and attribute stays, so the theme’s styles and scripts keep working, and Enter and Space now do what a click does. A short style reset for exactly those classes keeps their look. An icon-only button in the same banner or slideshow gets a name in English, as described under the icon names below. An element that no script listens to, one that the theme styles or scripts by its tag name, or an empty one whose purpose is unclear, is left as it is.

    Rules it serves
    • vs-carousel AUTO
    • vs-cookie-banner ASSISTED
    File types it changes
    .css.php
    turns the click-only cookie choices into buttons and names the close button Example data

    assets/css/main.css Changes to assets/css/main.css: 17 added, 0 removed

    Changes to assets/css/main.css: 17 added, 0 removed
    Line before Line after Change Code
    766 766 gap: 0.75rem;
    767 767 }
    768 768
    769 Added /* validiscan:button-semantics */
    770 Added /* Controls that became <button>s: keep the look of the elements they replace. The theme
    771 Added rules for these classes follow and still win. */
    769 772 .kb-cookie-bar__settings,
    770 773 .kb-cookie-bar__accept {
    774 Added margin: 0;
    775 Added padding: 0;
    776 Added border: 0;
    777 Added background: none;
    778 Added color: inherit;
    779 Added font: inherit;
    780 Added line-height: inherit;
    781 Added text-align: inherit;
    782 Added cursor: pointer;
    783 Added }
    784 Added /* /validiscan:button-semantics */
    785 Added
    786 Added .kb-cookie-bar__settings,
    787 Added .kb-cookie-bar__accept {
    771 788 padding: 0.6em 1.1em;
    772 789 border-radius: 4px;
    773 790 font-size: 0.9rem;

    template-parts/cookie-bar.php Changes to template-parts/cookie-bar.php: 3 added, 3 removed

    Changes to template-parts/cookie-bar.php: 3 added, 3 removed
    Line before Line after Change Code
    21 21 <a href="<?php echo esc_url( get_privacy_policy_url() ); ?>"><?php esc_html_e( 'Privacy policy', 'kestrel-birch' ); ?></a>
    22 22 </p>
    23 23 <div class="kb-cookie-bar__actions">
    24 Removed <span class="kb-cookie-bar__settings" data-kb-consent="settings"><?php esc_html_e( 'Cookie settings', 'kestrel-birch' ); ?></span>
    25 Removed <span class="kb-cookie-bar__accept" data-kb-consent="accept"><?php esc_html_e( 'Accept', 'kestrel-birch' ); ?></span>
    26 Removed <button type="button" class="kb-cookie-bar__close" data-kb-consent="close"><?php kb_icon( 'close' ); ?></button>
    24 Added <button class="kb-cookie-bar__settings" data-kb-consent="settings" type="button"><?php esc_html_e( 'Cookie settings', 'kestrel-birch' ); ?></button>
    25 Added <button class="kb-cookie-bar__accept" data-kb-consent="accept" type="button"><?php esc_html_e( 'Accept', 'kestrel-birch' ); ?></button>
    26 Added <button type="button" class="kb-cookie-bar__close" data-kb-consent="close"><?php kb_icon( 'close' ); ?><span class="screen-reader-text"><?php esc_html_e( 'Close', 'kestrel-birch' ); ?></span></button>
    27 27 </div>
    28 28 </div>

Zoom, tap targets and contrast

  • Zoom allowed on phones meta-viewport-zoom The viewport tag loses user-scalable=no and a maximum-scale below 2, so visitors can pinch-zoom your shop again, unless PHP computes the tag’s content or a plugin prints it.

    The recipe removes user-scalable=no and a maximum-scale below 2 from the viewport tag, in the header templates or in the PHP code that prints the tag, so visitors can pinch-zoom your shop again. For axe-core’s stricter rule meta-viewport-large, any maximum-scale below 5 is removed. The other viewport settings stay exactly as they were. A viewport tag whose content PHP computes, or one that a plugin prints, is left to a person.

    Rules it serves
    • meta-viewport AUTO
    • meta-viewport-large AUTO
    File types it changes
    .php
    re-enables zoom in every header template of the fixture theme Example data

    header-checkout.php Changes to header-checkout.php: 1 added, 1 removed

    Changes to header-checkout.php: 1 added, 1 removed
    Line before Line after Change Code
    12 12 <head>
    13 13 <meta charset="<?php bloginfo( 'charset' ); ?>">
    14 14 <?php // VALIDISCAN-SEED meta-viewport: zooming is disabled. ?>
    15 Removed <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    15 Added <meta name="viewport" content="width=device-width, initial-scale=1">
    16 16 <?php wp_head(); ?>
    17 17 </head>
    18 18

    header.php Changes to header.php: 1 added, 1 removed

    Changes to header.php: 1 added, 1 removed
    Line before Line after Change Code
    14 14 <head>
    15 15 <meta charset="<?php bloginfo( 'charset' ); ?>">
    16 16 <?php // VALIDISCAN-SEED meta-viewport: zooming is disabled ("stops iOS zooming into inputs"). ?>
    17 Removed <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    17 Added <meta name="viewport" content="width=device-width, initial-scale=1">
    18 18 <?php wp_head(); ?>
    19 19 </head>
    20 20
  • Larger tap targets target-size-padding Fixed widths or heights below 24 pixels on controls the scan found too small are raised to 24 pixels.

    Controls that the scan found smaller than 24 by 24 pixels, such as social icons, colour swatches or quantity buttons, get that minimum size. The recipe finds the stylesheet rules that give exactly those elements a fixed width or height below 24 pixels and raises it to 24 pixels; where the element lays out its icon with flexbox or grid, the icon is centred so it does not sit in a corner. A control whose size comes from padding or from its content cannot be measured from the stylesheets and is left to a person.

    Rules it serves
    • vs-target-size AUTO
    File types it changes
    .css
    raises the social icons, quantity steppers and swatches of the fixture theme to 24 × 24 px Example data

    assets/css/main.css Changes to assets/css/main.css: 4 added, 2 removed

    Changes to assets/css/main.css: 4 added, 2 removed
    Line before Line after Change Code
    710 710 /* VALIDISCAN-SEED vs-target-size: 18 x 18 px icon links, 4 px apart. */
    711 711 .kb-social__link {
    712 712 display: inline-flex;
    713 Removed width: 18px;
    714 Removed height: 18px;
    713 Added width: 24px;
    714 Added height: 24px;
    715 715 color: var(--kb-ink);
    716 Added align-items: center;
    717 Added justify-content: center;
    716 718 }
    717 719
    718 720 .site-info {

    assets/css/woocommerce.css Changes to assets/css/woocommerce.css: 4 added, 4 removed

    Changes to assets/css/woocommerce.css: 4 added, 4 removed
    Line before Line after Change Code
    421 421
    422 422 /* VALIDISCAN-SEED vs-target-size: 18 x 18 px colour swatches, 4 px apart. */
    423 423 .kb-swatch {
    424 Removed width: 18px;
    425 Removed height: 18px;
    424 Added width: 24px;
    425 Added height: 24px;
    426 426 padding: 0;
    427 427 border: 1px solid rgba(0, 0, 0, 0.2);
    428 428 border-radius: 50%;
    489 489 /* VALIDISCAN-SEED vs-target-size: 18 x 18 px quantity steppers glued to the input. */
    490 490 .kb-qty-minus,
    491 491 .kb-qty-plus {
    492 Removed width: 18px;
    493 Removed height: 18px;
    492 Added width: 24px;
    493 Added height: 24px;
    494 494 padding: 0;
    495 495 border: 1px solid #b9b3aa;
    496 496 border-radius: 2px;
  • Text colours with enough contrast contrast-tokens A failing text colour defined in one place, and not also used for backgrounds, borders or elements the scan did not measure, moves to the nearest shade of its hue that reaches the required contrast.

    When text fails the contrast check and its colour is defined in one place in the theme’s stylesheets, as a CSS variable or as the colour of the rule that styles that text, that colour moves to the nearest lighter or darker shade of the same hue that reaches the contrast the scan measured as required, on every background it measured. A colour that is defined in several places, that would also change elements whose contrast the scan did not measure, or that the theme also uses for backgrounds or borders, is left to a person.

    Rules it serves
    • color-contrast ASSISTED
    File types it changes
    .css
    darkens the footer text colour when every element it styles was measured Example data

    assets/css/main.css Changes to assets/css/main.css: 1 added, 1 removed

    Changes to assets/css/main.css: 1 added, 1 removed
    Line before Line after Change Code
    676 676 .kb-footer-about,
    677 677 .kb-footer-newsletter-intro,
    678 678 .site-info {
    679 Removed color: #888;
    679 Added color: #6e6e6e;
    680 680 }
    681 681
    682 682 .site-footer ul.menu {

Motion, slideshows and sound

  • Less motion when a visitor asks for it reduced-motion-guard For visitors whose device asks for reduced motion, animations stop in their final state and a slideshow autoplay that the theme’s own scripts start stays off; motion from a plugin or an inline script is not changed.

    When a visitor’s device is set to reduce motion, a block at the end of the theme’s main stylesheet lets each animation run once, too briefly to see, so it stops in its final state; transitions become instant and smooth scrolling is turned off. Where the theme’s own scripts start a slideshow’s autoplay, with Slick, Swiper, Splide, Owl or Flickity, the autoplay starts only for visitors who have not asked for reduced motion. Motion that comes from a plugin or an inline script is not changed.

    Rules it serves
    • vs-motion AUTO
    File types it changes
    .css
    guards the looping hero slide and sale badge animations of the fixture theme Example data

    assets/css/main.css Changes to assets/css/main.css: 13 added, 0 removed

    Changes to assets/css/main.css: 13 added, 0 removed
    Line before Line after Change Code
    794 794 color: #999;
    795 795 cursor: pointer;
    796 796 }
    797 Added
    798 Added /* validiscan:reduced-motion-guard */
    799 Added @media (prefers-reduced-motion: reduce) {
    800 Added *,
    801 Added *::before,
    802 Added *::after {
    803 Added animation-duration: 0.01ms !important;
    804 Added animation-iteration-count: 1 !important;
    805 Added transition-duration: 0.01ms !important;
    806 Added scroll-behavior: auto !important;
    807 Added }
    808 Added }
    809 Added /* /validiscan:reduced-motion-guard */
  • A pause button for moving slideshows slider-pause-control A slideshow in your theme’s templates that moves on its own gets a visible pause and play button, labelled in English, unless a plugin or an inline script drives it.

    A slideshow in your theme’s templates that moves on its own gets a visible button to pause and play it, labelled “Pause slideshow” and “Play slideshow” in English. The button becomes the slideshow’s first element, so keyboard users reach it before the slides. A short script in the theme’s main script file connects the button to the slider’s own autoplay: a CSS animation of the theme, Slick, Swiper or Flickity. For visitors whose device asks for reduced motion, the slideshow starts paused. A slider that a plugin or an inline script drives is left to a person.

    Rules it serves
    • vs-carousel AUTO
    File types it changes
    .css.js.php
    adds a pause button to the CSS-animated hero slider of the fixture theme Example data

    assets/css/main.css Changes to assets/css/main.css: 28 added, 0 removed

    Changes to assets/css/main.css: 28 added, 0 removed
    Line before Line after Change Code
    508 508 background: var(--kb-navy);
    509 509 }
    510 510
    511 Added /* validiscan:slider-pause-control */
    512 Added .hero-slider.is-paused .hero-slider__track {
    513 Added animation-play-state: paused;
    514 Added }
    515 Added
    516 Added @media (prefers-reduced-motion: reduce) {
    517 Added .hero-slider__track {
    518 Added animation-play-state: paused;
    519 Added }
    520 Added }
    521 Added
    522 Added .hero-slider__pause {
    523 Added position: absolute;
    524 Added top: 1rem;
    525 Added right: 1rem;
    526 Added z-index: 2;
    527 Added min-height: 24px;
    528 Added padding: 0.4em 0.9em;
    529 Added border: 2px solid currentColor;
    530 Added border-radius: 4px;
    531 Added background: #ffffff;
    532 Added color: var(--kb-ink);
    533 Added font: inherit;
    534 Added font-weight: 600;
    535 Added cursor: pointer;
    536 Added }
    537 Added /* /validiscan:slider-pause-control */
    538 Added
    511 539 .hero-slide img {
    512 540 width: 100%;
    513 541 aspect-ratio: 5 / 2;

    assets/js/theme.js Changes to assets/js/theme.js: 20 added, 0 removed

    Changes to assets/js/theme.js: 20 added, 0 removed
    Line before Line after Change Code
    255 255 initCheckout();
    256 256 initShowPassword();
    257 257 })();
    258 Added
    259 Added /* validiscan:slider-pause-control */
    260 Added (function () {
    261 Added const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    262 Added for (const slider of document.querySelectorAll('.hero-slider')) {
    263 Added const button = slider.querySelector('.hero-slider__pause');
    264 Added if (!button) continue;
    265 Added const pauseLabel = button.textContent;
    266 Added const playLabel = button.getAttribute('data-play-label') || pauseLabel;
    267 Added const set = (paused) => {
    268 Added slider.classList.toggle('is-paused', paused);
    269 Added button.textContent = paused ? playLabel : pauseLabel;
    270 Added };
    271 Added button.addEventListener('click', () => {
    272 Added set(!slider.classList.contains('is-paused'));
    273 Added });
    274 Added if (reduce) set(true);
    275 Added }
    276 Added })();
    277 Added /* /validiscan:slider-pause-control */

    template-parts/hero-slider.php Changes to template-parts/hero-slider.php: 1 added, 0 removed

    Changes to template-parts/hero-slider.php: 1 added, 0 removed
    Line before Line after Change Code
    37 37 };
    38 38 ?>
    39 39 <div class="hero-slider" data-autoplay="true" data-interval="6000">
    40 Added <button type="button" class="hero-slider__pause" data-play-label="<?php esc_attr_e( 'Play slideshow', 'kestrel-birch' ); ?>"><?php esc_html_e( 'Pause slideshow', 'kestrel-birch' ); ?></button>
    40 41 <div class="hero-slider__viewport">
    41 42 <div class="hero-slider__track">
    42 43 <?php
  • 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 a script starts it; on WooCommerce 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 plays inside the page on iPhones instead of in full screen; where the video has controls, visitors can still turn the sound on. Audio that starts by itself no longer does. Media that a script starts are 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 is MANUAL (see the list further down), so on WooCommerce 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
    .php
    mutes an autoplaying product video of the fixture theme Example data

    woocommerce/single-product/tabs/description.php Changes to woocommerce/single-product/tabs/description.php: 1 added, 1 removed

    Changes to woocommerce/single-product/tabs/description.php: 1 added, 1 removed
    Line before Line after Change Code
    48 48 <?php if ( $kb_video ) : ?>
    49 49 <h3><?php esc_html_e( 'The weekender, packed', 'kestrel-birch' ); ?></h3>
    50 50 <figure class="wp-block-video">
    51 Removed <video autoplay loop controls preload="none" poster="<?php echo esc_url( get_the_post_thumbnail_url( $kb_video, 'full' ) ); ?>" width="640" height="360">
    51 Added <video autoplay loop controls preload="none" poster="<?php echo esc_url( get_the_post_thumbnail_url( $kb_video, 'full' ) ); ?>" width="640" height="360" muted playsinline>
    52 52 <source src="<?php echo esc_url( wp_get_attachment_url( $kb_video ) ); ?>" type="video/webm">
    53 53 </video>
    54 54 <figcaption><?php echo esc_html( wp_get_attachment_caption( $kb_video ) ); ?></figcaption>

Forms, prices and the cart

  • Labels connected to their fields label-association A label already in the template but not connected to its field gets connected, through for or aria-labelledby.

    A form field whose label text is already in the template, but not connected to the field, gets connected: the <label> right before the field, or right after a checkbox or radio button, gets a for that points at the field, or a text element marked as a label is connected through aria-labelledby. Screen readers then read the label when the field gets focus, and clicking a connected <label> moves the focus to the field. 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
    .php
    ties an unassociated coupon label to its field, as in the fixed cart Example data

    woocommerce/cart/cart.php Changes to woocommerce/cart/cart.php: 1 added, 1 removed

    Changes to woocommerce/cart/cart.php: 1 added, 1 removed
    Line before Line after Change Code
    62 62 <tr>
    63 63 <td colspan="6" class="actions">
    64 64 <div class="coupon">
    65 Removed <label class="screen-reader-text"><?php esc_html_e( 'Coupon:', 'woocommerce' ); ?></label>
    65 Added <label class="screen-reader-text" for="coupon_code"><?php esc_html_e( 'Coupon:', 'woocommerce' ); ?></label>
    66 66 <input type="text" name="coupon_code" class="input-text" id="coupon_code" value="" placeholder="<?php esc_attr_e( 'Coupon code', 'woocommerce' ); ?>">
    67 67 <button type="submit" class="button" name="apply_coupon" value="<?php esc_attr_e( 'Apply coupon', 'woocommerce' ); ?>"><?php esc_html_e( 'Apply coupon', 'woocommerce' ); ?></button>
    68 68 </div>
  • Checkout and account fields that browsers can fill in autocomplete-tokens Fields that ask for data about the customer get the matching autocomplete value where the code shows their purpose; for WooCommerce’s billing and shipping fields, a filter in functions.php sets it.

    Fields in the theme’s templates that ask for data about the customer, such as login, registration and checkout fields, get the autocomplete value for what they ask for, such as email, username, current or new password, first and last name, company, address, postal code, country or phone number. The value is worked out from the field’s type, name, id and label. A value that is missing, off or invalid is replaced; a valid one stays. WooCommerce’s own billing and shipping fields are printed by WooCommerce, not by the theme, so a filter in functions.php sets their values. It runs after the theme’s own filters for those fields, which can remove the values. Fields that ask for no personal data, such as search, coupon or quantity fields, are left as they are, and so is a field whose purpose the code does not show.

    Rules it serves
    • autocomplete-valid AUTO checked through vs-forms-ecommerce
    • vs-forms-ecommerce AUTO
    File types it changes
    .php
    sets the tokens of the login, registration and checkout fields of the fixture theme Example data

    functions.php Changes to functions.php: 44 added, 0 removed

    Changes to functions.php: 44 added, 0 removed
    Line before Line after Change Code
    308 308 * here; the remediation adds one for pages with the _kb_content_language meta and marks the
    309 309 * English header and footer with lang="en-GB".
    310 310 */
    311 Added
    312 Added /* validiscan:autocomplete-tokens */
    313 Added /**
    314 Added * Autocomplete tokens for the WooCommerce address fields, so browsers and assistive
    315 Added * technology know what each field asks for (WCAG 1.3.5). Runs after the theme's own
    316 Added * field filters.
    317 Added *
    318 Added * @param array $fields Checkout fields (grouped by section) or one address section.
    319 Added * @return array
    320 Added */
    321 Added function kb_address_autocomplete( $fields ) {
    322 Added $tokens = array(
    323 Added 'first_name' => 'given-name',
    324 Added 'last_name' => 'family-name',
    325 Added 'company' => 'organization',
    326 Added 'country' => 'country',
    327 Added 'address_1' => 'address-line1',
    328 Added 'address_2' => 'address-line2',
    329 Added 'city' => 'address-level2',
    330 Added 'state' => 'address-level1',
    331 Added 'postcode' => 'postal-code',
    332 Added 'phone' => 'tel',
    333 Added 'email' => 'email',
    334 Added );
    335 Added foreach ( $fields as $key => $field ) {
    336 Added if ( ! is_array( $field ) ) {
    337 Added continue;
    338 Added }
    339 Added if ( 'billing' === $key || 'shipping' === $key ) {
    340 Added $fields[ $key ] = kb_address_autocomplete( $field );
    341 Added continue;
    342 Added }
    343 Added if ( ! preg_match( '/^(billing|shipping)_(.+)$/', (string) $key, $match ) || ! isset( $tokens[ $match[2] ] ) ) {
    344 Added continue;
    345 Added }
    346 Added $fields[ $key ]['autocomplete'] = ( 'shipping' === $match[1] ? 'shipping ' : '' ) . $tokens[ $match[2] ];
    347 Added unset( $fields[ $key ]['custom_attributes']['autocomplete'] );
    348 Added }
    349 Added return $fields;
    350 Added }
    351 Added add_filter( 'woocommerce_checkout_fields', 'kb_address_autocomplete', 99 );
    352 Added add_filter( 'woocommerce_billing_fields', 'kb_address_autocomplete', 99 );
    353 Added add_filter( 'woocommerce_shipping_fields', 'kb_address_autocomplete', 99 );
    354 Added /* /validiscan:autocomplete-tokens */

    woocommerce/myaccount/form-login.php Changes to woocommerce/myaccount/form-login.php: 3 added, 3 removed

    Changes to woocommerce/myaccount/form-login.php: 3 added, 3 removed
    Line before Line after Change Code
    29 29
    30 30 <p class="woocommerce-form-row woocommerce-form-row--wide form-row form-row-wide">
    31 31 <label for="username"><?php esc_html_e( 'Username or email address', 'woocommerce' ); ?>&nbsp;<span class="required">*</span></label>
    32 Removed <input type="text" class="woocommerce-Input woocommerce-Input--text input-text" name="username" id="username" autocomplete="off" value="<?php echo ( ! empty( $_POST['username'] ) ) ? esc_attr( wp_unslash( $_POST['username'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ?>">
    32 Added <input type="text" class="woocommerce-Input woocommerce-Input--text input-text" name="username" id="username" autocomplete="username" value="<?php echo ( ! empty( $_POST['username'] ) ) ? esc_attr( wp_unslash( $_POST['username'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ?>">
    33 33 </p>
    34 34 <p class="woocommerce-form-row woocommerce-form-row--wide form-row form-row-wide">
    35 35 <label for="password"><?php esc_html_e( 'Password', 'woocommerce' ); ?>&nbsp;<span class="required">*</span></label>
    36 36 <span class="password-input">
    37 Removed <input class="woocommerce-Input woocommerce-Input--text input-text" type="password" name="password" id="password" autocomplete="off" onpaste="return false;">
    37 Added <input class="woocommerce-Input woocommerce-Input--text input-text" type="password" name="password" id="password" autocomplete="current-password" onpaste="return false;">
    38 38 <button type="button" class="show-password-input"><?php kb_icon( 'eye' ); ?></button>
    39 39 </span>
    40 40 </p>
    63 63 <?php do_action( 'woocommerce_register_form_start' ); ?>
    64 64
    65 65 <p class="woocommerce-form-row woocommerce-form-row--wide form-row form-row-wide">
    66 Removed <input type="email" class="woocommerce-Input woocommerce-Input--text input-text" name="email" id="reg_email" autocomplete="off" value="" placeholder="<?php esc_attr_e( 'Email address *', 'kestrel-birch' ); ?>">
    66 Added <input type="email" class="woocommerce-Input woocommerce-Input--text input-text" name="email" id="reg_email" autocomplete="email" value="" placeholder="<?php esc_attr_e( 'Email address *', 'kestrel-birch' ); ?>">
    67 67 </p>
    68 68 <p><?php esc_html_e( 'A link to set a new password will be sent to your email address.', 'woocommerce' ); ?></p>
    69 69
  • Price, stock and cart changes announced live-region-cart-variant Screen readers announce the new price and stock of a chosen variation, unless the theme’s script replaces that area as a whole, and, in English, a product added to or removed from the cart.

    When a customer picks another size or colour, the area that shows the price and stock of the chosen variation is rewritten in place. That area becomes a live region, so screen readers announce the new price and stock without the customer having to move there. The cart link in the header is replaced as a whole when a product is added, so it cannot carry the announcement itself. Next to it the recipe adds a visually hidden status message, and a short block in the theme’s script writes “Product added to your cart.” or “Product removed from your cart.” into it when WooCommerce reports the change. Both messages are added in English. An area that already is or sits inside a live region, or that the theme’s own script replaces as a whole, is left as it is.

    Rules it serves
    • vs-live-updates AUTO
    File types it changes
    .js.php
    adds a status region next to the header cart fragment and wires the theme script Example data

    assets/js/theme.js Changes to assets/js/theme.js: 15 added, 0 removed

    Changes to assets/js/theme.js: 15 added, 0 removed
    Line before Line after Change Code
    255 255 initCheckout();
    256 256 initShowPassword();
    257 257 })();
    258 Added
    259 Added /* validiscan:live-region-cart-variant */
    260 Added // Announces cart changes in the status region next to the header cart (WCAG 4.1.3).
    261 Added (function () {
    262 Added const region = document.querySelector('[data-kb-cart-status]');
    263 Added if (!region || !window.jQuery) return;
    264 Added window.jQuery(document.body).on('added_to_cart removed_from_cart', (event) => {
    265 Added const message = region.getAttribute(event.type === 'added_to_cart' ? 'data-added' : 'data-removed');
    266 Added region.textContent = '';
    267 Added window.setTimeout(() => {
    268 Added region.textContent = message || '';
    269 Added }, 100);
    270 Added });
    271 Added })();
    272 Added /* /validiscan:live-region-cart-variant */

    header.php Changes to header.php: 1 added, 0 removed

    Changes to header.php: 1 added, 0 removed
    Line before Line after Change Code
    67 67 <?php kb_icon( 'bag' ); ?><span class="kb-cart-amount"><?php echo wp_kses_post( WC()->cart->get_cart_subtotal() ); ?></span>
    68 68 <span class="count"><?php echo esc_html( sprintf( _n( '%d item', '%d items', WC()->cart->get_cart_contents_count(), 'storefront' ), WC()->cart->get_cart_contents_count() ) ); ?></span>
    69 69 </a>
    70 Added <span class="screen-reader-text" role="status" data-kb-cart-status data-added="<?php esc_attr_e( 'Product added to your cart.', 'kestrel-birch' ); ?>" data-removed="<?php esc_attr_e( 'Product removed from your cart.', 'kestrel-birch' ); ?>"></span>
    70 71 </li>
    71 72 </ul>
    72 73 </div>
    makes the variation price and stock of the fixture theme a polite live region Example data

    woocommerce/single-product/add-to-cart/variable.php Changes to woocommerce/single-product/add-to-cart/variable.php: 1 added, 1 removed

    Changes to woocommerce/single-product/add-to-cart/variable.php: 1 added, 1 removed
    Line before Line after Change Code
    65 65 </table>
    66 66
    67 67 <div class="single_variation_wrap">
    68 Removed <div class="woocommerce-variation single_variation">
    68 Added <div class="woocommerce-variation single_variation" aria-live="polite" aria-atomic="true">
    69 69 <div class="woocommerce-variation-description"></div>
    70 70 <div class="woocommerce-variation-price"><span class="price"><?php echo wp_kses_post( wc_price( $kb_selected['display_price'] ) ); ?></span></div>
    71 71 <div class="woocommerce-variation-availability"><?php echo wp_kses_post( $kb_selected['availability_html'] ); ?></div>

Names for icons, images and embeds

  • Names for icon-only buttons and links icon-accessible-name Buttons and links that show only an icon get a name in English that says what they do, where the code shows their purpose.

    Buttons and links that show only an icon get a name that says what they do, where the code shows their purpose: from the icon’s own name, from the control’s class or id, or from where a link goes, such as the cart, the account page or a social network. Examples are Search, Cart, My account, Show password, Close and Menu. The name is visually hidden text inside the control, or an aria-label where the theme has no class for hidden text, and it is added in English. Social network names are brand names and stay as they are. A control whose purpose the code does not show is left to a person.

    Rules it serves
    • button-name ASSISTED
    • link-name ASSISTED
    File types it changes
    .php
    names the account link and the social links, and leaves the uncertain gallery link Example data

    header.php Changes to header.php: 1 added, 1 removed

    Changes to header.php: 1 added, 1 removed
    Line before Line after Change Code
    60 60 <ul id="site-header-cart" class="site-header-cart menu">
    61 61 <li class="kb-account">
    62 62 <?php // VALIDISCAN-SEED link-name: icon-only account link. ?>
    63 Removed <a class="kb-account-link" href="<?php echo esc_url( wc_get_page_permalink( 'myaccount' ) ); ?>"><?php kb_icon( 'user' ); ?></a>
    63 Added <a class="kb-account-link" href="<?php echo esc_url( wc_get_page_permalink( 'myaccount' ) ); ?>"><?php kb_icon( 'user' ); ?><span class="screen-reader-text"><?php esc_html_e( 'My account', 'kestrel-birch' ); ?></span></a>
    64 64 </li>
    65 65 <li class="kb-cart<?php echo is_cart() ? ' current-menu-item' : ''; ?>">
    66 66 <a class="cart-contents" href="<?php echo esc_url( wc_get_cart_url() ); ?>" title="<?php esc_attr_e( 'View your shopping cart', 'storefront' ); ?>">

    template-parts/social-links.php Changes to template-parts/social-links.php: 1 added, 1 removed

    Changes to template-parts/social-links.php: 1 added, 1 removed
    Line before Line after Change Code
    17 17 ?>
    18 18 <ul class="kb-social">
    19 19 <?php foreach ( $kb_profiles as $kb_network => $kb_url ) : ?>
    20 Removed <a class="kb-social__link" href="<?php echo esc_url( $kb_url ); ?>"><?php kb_icon( $kb_network ); ?></a>
    20 Added <a class="kb-social__link" href="<?php echo esc_url( $kb_url ); ?>"><?php kb_icon( $kb_network ); ?><span class="screen-reader-text"><?php echo esc_html( ucfirst( $kb_network ) ); ?></span></a>
    21 21 <?php endforeach; ?>
    22 22 </ul>
    names the icon-only search and show-password buttons of the fixture theme Example data

    header.php Changes to header.php: 1 added, 1 removed

    Changes to header.php: 1 added, 1 removed
    Line before Line after Change Code
    51 51 <?php // VALIDISCAN-SEED vs-focus-order + tabindex: tabindex="1" "so the search is always the first stop". ?>
    52 52 <input type="search" id="woocommerce-product-search-field-0" class="search-field" placeholder="<?php echo esc_attr__( 'Search products&hellip;', 'woocommerce' ); ?>" value="<?php echo get_search_query(); ?>" name="s" tabindex="1">
    53 53 <?php // VALIDISCAN-SEED button-name: icon-only submit button without text. ?>
    54 Removed <button type="submit" value="<?php echo esc_attr_x( 'Search', 'submit button', 'woocommerce' ); ?>" class="kb-search-submit"><?php kb_icon( 'search' ); ?></button>
    54 Added <button type="submit" value="<?php echo esc_attr_x( 'Search', 'submit button', 'woocommerce' ); ?>" class="kb-search-submit"><?php kb_icon( 'search' ); ?><span class="screen-reader-text"><?php esc_html_e( 'Search', 'kestrel-birch' ); ?></span></button>
    55 55 <input type="hidden" name="post_type" value="product">
    56 56 </form>
    57 57 </div>

    woocommerce/myaccount/form-login.php Changes to woocommerce/myaccount/form-login.php: 1 added, 1 removed

    Changes to woocommerce/myaccount/form-login.php: 1 added, 1 removed
    Line before Line after Change Code
    35 35 <label for="password"><?php esc_html_e( 'Password', 'woocommerce' ); ?>&nbsp;<span class="required">*</span></label>
    36 36 <span class="password-input">
    37 37 <input class="woocommerce-Input woocommerce-Input--text input-text" type="password" name="password" id="password" autocomplete="off" onpaste="return false;">
    38 Removed <button type="button" class="show-password-input"><?php kb_icon( 'eye' ); ?></button>
    38 Added <button type="button" class="show-password-input"><?php kb_icon( 'eye' ); ?><span class="screen-reader-text"><?php esc_html_e( 'Show password', 'kestrel-birch' ); ?></span></button>
    39 39 </span>
    40 40 </p>
    41 41
  • Decorative images hidden from screen readers decorative-image-alt Only images the code proves decorative get an empty alt or aria-hidden; the text alternative of every other image is left to a person.

    An image is marked as decorative only when the code proves it adds nothing: it sits in a link or button that has its own text, such as a logo next to the shop name, it is a small icon 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 announcing an unnamed graphic. Every other image is content, and its text alternative is left to a person.

    Rules it serves
    • image-alt ASSISTED
    • role-img-alt ASSISTED
    • svg-img-alt ASSISTED
    File types it changes
    .php
    hides the logo mark inside the home link that has the shop name Example data

    header.php Changes to header.php: 1 added, 1 removed

    Changes to header.php: 1 added, 1 removed
    Line before Line after Change Code
    39 39 <div class="site-branding">
    40 40 <a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="custom-logo-link" rel="home">
    41 41 <?php // VALIDISCAN-SEED svg-img-alt: the logo mark is role="img" without a title or aria-label. ?>
    42 Removed <svg class="kb-logo-mark" role="img" width="40" height="40" viewBox="0 0 40 40"><path d="M3 30c6-11 15-17 29-18-7 3-10 8-11 13 6-4 11-5 17-4-9 2-15 8-19 15L3 30Z" fill="#1d3b53"/></svg>
    42 Added <svg class="kb-logo-mark" width="40" height="40" viewBox="0 0 40 40" aria-hidden="true" focusable="false"><path d="M3 30c6-11 15-17 29-18-7 3-10 8-11 13 6-4 11-5 17-4-9 2-15 8-19 15L3 30Z" fill="#1d3b53"/></svg>
    43 43 <span class="kb-logo-text"><?php bloginfo( 'name' ); ?></span>
    44 44 </a>
    45 45 </div>
  • Titles for embedded maps and videos iframe-title An embedded frame without a title gets an English title that says what it shows, where the code shows it.

    An embedded frame without a title, such as a map or a YouTube or Vimeo video, gets a title in English that says what it shows. The recipe takes it from the title of an embedded file in the theme, from the provider in the frame’s address, or from words such as map or video in its class or id. A frame whose content the code does not show gets no generic title; it is left to a person.

    Rules it serves
    • frame-title AUTO
    File types it changes
    .php
    titles the store map iframe of the contact page from the embedded document Example data

    page-contact.php Changes to page-contact.php: 1 added, 1 removed

    Changes to page-contact.php: 1 added, 1 removed
    Line before Line after Change Code
    81 81
    82 82 <div class="kb-store-map">
    83 83 <?php // VALIDISCAN-SEED frame-title: the map iframe has no title. ?>
    84 Removed <iframe src="<?php echo esc_url( get_stylesheet_directory_uri() . '/assets/embeds/store-map.html' ); ?>" width="600" height="320" loading="lazy"></iframe>
    84 Added <iframe src="<?php echo esc_url( get_stylesheet_directory_uri() . '/assets/embeds/store-map.html' ); ?>" width="600" height="320" loading="lazy" title="<?php esc_attr_e( 'Map: Kestrel & Birch workshop, Hamburg', 'kestrel-birch' ); ?>"></iframe>
    85 85 </div>
    86 86 </div>
    87 87 </article>
Details: what the recipes fix, the tiers, and the test theme

Validiscan has 20 recipes for WooCommerce themes. They fix page structure and navigation, keyboard focus, zoom, tap targets and contrast, motion and sound, form fields and the cart, and the names of icons, images and embedded frames. A recipe is a deterministic change for one kind of failure: the same code always gets the same change. 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 and the finding is left to a person.

Under each recipe you find the rules it serves, with each rule’s tier in Validiscan’s catalogue. 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 Validiscan gives guidance instead of 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 marked “checked through” another check is not run on its own in a scan: that check reports the same failure.

The file types and the before and after of each recipe come from the application’s own tests. Most of them run on its WooCommerce test theme, a child theme of the Storefront theme with accessibility failures put in on purpose; the comments marked VALIDISCAN-SEED that you see in some diffs point them out and exist only in the test theme. WordPress pages cannot be rendered in the test environment, so these tests do not render pages: they check the change itself, as a before and after of the theme’s files. Your theme’s files will look different.

What stays MANUAL on WooCommerce?

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 WooCommerce. 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 WooCommerce; 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: the focus order check, 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, another way to use a control that only works by dragging, or a review of linked documents such as PDFs. Where a vs-focus-order finding comes from a tabindex above 0 in a theme template, the tab order recipe above removes it.

The work on MANUAL findings that need your own content decisions is not part of a project, and neither are PDFs. Between the rules of tier AUTO and those of tier MANUAL are the rules of tier ASSISTED, such as text alternatives for product images or names for buttons: their fixes need judgement, and Validiscan reviews every fix before it is delivered. 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 whether AI may draft ASSISTED fixes, a developer or agency who reviews and puts the changes live, and your decisions on what stays MANUAL; for a shop in another language, translations of the new English text.

  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 your theme or child theme, or a ZIP of the theme folder 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 or agency who reviews the changes, merges the pull request or applies the patch bundle, and puts the theme live.
  5. Translations of the new English text, if your shop is in another language.
  6. 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.

Sources

4 sources, each checked for this page on 6 October 2026.

WooCommerce and WordPress developer documentation 4
  1. Template structure & Overriding templates via a theme · WooCommerce · checked
  2. language_attributes() · WordPress.org · checked
  3. Localization (Common APIs Handbook) · WordPress.org · checked
  4. Templates (Theme Handbook) · WordPress.org · 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