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:
On this page: 7 answers
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.
| 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/, 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.
- Validiscan delivers the changes it has reviewed and approved: a pull request on a new branch, or a patch bundle for your developer.
- Your developer 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.
- You or your developer merge the pull request, or apply the patch bundle and test the changes.
- You or your developer put the theme live.
- Once the changes are live, a verification scan checks the affected pages again.
A pull request
With access to your repository, Validiscan creates a new branch named validiscan/, commits each fix on its own and opens one pull request with a summary table, an explanation of each change and a review checklist. Nothing is pushed to your default branch. The checklist asks your developer to deploy the branch to a 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.
- Regenerate the theme’s
.potfile. - Add the translations to its
.pofiles. - Compile the
.mofiles 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 fixedlangthat is not valid, gets WordPress’slanguage_attributes(), which prints the site language.The recipe makes the
<html>element of your header templates take its language from WordPress’slanguage_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 fixedlangvalue 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 infunctions.phpdeclares 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-langAUTO checked through vs-lang -
html-lang-validAUTO checked through vs-lang -
vs-langAUTO
-
- 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. ?> -
Skip link, main area and named menus skip-link-main A “Skip to content” link where a header template has none, a
<main>area from the page’s content wrapper where none exists, and English names for navigation areas whose purpose shows.Each header template without a skip link gets a “Skip to content” link right after the opening
<body>tag, or after thewp_body_open()call that follows it. It stays hidden until it receives focus and jumps to the page’s content. If your theme is a child theme of Storefront and switched off Storefront’s skip links, they are switched back on instead. Where the templates have no main area yet, the content wrapper of the page templates, a<div>with the idmainor the classsite-main, becomes a<main>element, which marks it as the page’s main area; where the theme’s styles or scripts select that wrapper as adiv, it stays as it is. Navigation areas whose purpose shows in their id or class get a name, such as Primary navigation, Breadcrumb, Footer navigation or Pagination. An existing skip link that points nowhere is pointed at the content. The link text and the names are added in English.- Rules it serves
-
-
bypassAUTO checked through vs-headings-landmarks -
landmark-one-mainAUTO checked through vs-headings-landmarks -
regionAUTO -
skip-linkAUTO -
vs-headings-landmarksAUTO -
landmark-uniqueASSISTED
-
- File types it changes
-
.php
restores the skip links, adds <main> and labels the navigation of the fixture theme Example data 404.php Changes to 404.php: 2 added, 2 removed
Changes to 404.php: 2 added, 2 removed Line before Line after Change Code 9 9 ?> 10 10 11 11 <div id="primary" class="content-area"> 12 Removed <div id="main" class="site-main"> 12 Added <main id="main" class="site-main"> 13 13 <div class="error-404 not-found"> 14 14 <div class="page-content"> 15 15 <header class="page-header"> 22 22 </section> 23 23 </div> 24 24 </div> 25 Removed </div><!-- #main --> 25 Added </main><!-- #main --> 26 26 </div><!-- #primary --> 27 27 28 28 <?php archive-product.php Changes to archive-product.php: 2 added, 2 removed
Changes to archive-product.php: 2 added, 2 removed Line before Line after Change Code 22 22 23 23 <div id="primary" class="content-area"> 24 24 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: <div> instead of <main>. ?> 25 Removed <div id="main" class="site-main"> 25 Added <main id="main" class="site-main"> 26 26 27 27 <header class="woocommerce-products-header"> 28 28 <h1 class="woocommerce-products-header__title page-title"><?php woocommerce_page_title(); ?></h1> 46 46 <?php do_action( 'woocommerce_no_products_found' ); ?> 47 47 <?php endif; ?> 48 48 49 Removed </div><!-- #main --> 49 Added </main><!-- #main --> 50 50 </div><!-- #primary --> 51 51 52 52 <?php // The sidebar div gets role="complementary" but no label (VALIDISCAN-SEED landmark-unique on pages with two). ?> 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 18 18 19 19 <div id="primary" class="content-area"> 20 20 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: the content wrapper is a <div>, not <main>. ?> 21 Removed <div id="main" class="site-main"> 21 Added <main id="main" class="site-main"> 22 22 23 23 <?php get_template_part( 'template-parts/hero-slider' ); ?> 24 24 62 62 ?> 63 63 </section> 64 64 65 Removed </div><!-- #main --> 65 Added </main><!-- #main --> 66 66 </div><!-- #primary --> 67 67 68 68 <?php functions.php Changes to functions.php: 1 added, 4 removed
Changes to functions.php: 1 added, 4 removed Line before Line after Change Code 32 32 * Storefront pieces the child theme does not use. 33 33 */ 34 34 function kb_remove_storefront_parts() { 35 Removed // VALIDISCAN-SEED vs-headings-landmarks: Storefront's "Skip to navigation / Skip to content" 36 Removed // links are removed because "they flash on the first Tab"; nothing replaces them. 37 Removed remove_action( 'storefront_before_header', 'storefront_skip_links', 0 ); 38 35 remove_action( 'storefront_footer', 'storefront_handheld_footer_bar', 999 ); 39 36 remove_action( 'storefront_header', 'storefront_product_search', 40 ); 40 37 } 212 209 $defaults['delimiter'] = '<span class="breadcrumb-separator"> / </span>'; 213 210 // VALIDISCAN-SEED landmark-unique + vs-headings-landmarks: the breadcrumb <nav> has no aria-label, 214 211 // like the primary and the footer navigation. 215 Removed $defaults['wrap_before'] = '<div class="storefront-breadcrumb"><div class="col-full"><nav class="woocommerce-breadcrumb">'; 212 Added $defaults['wrap_before'] = '<div class="storefront-breadcrumb"><div class="col-full"><nav class="woocommerce-breadcrumb" aria-label="' . esc_attr__( 'Breadcrumb', 'kestrel-birch' ) . '">'; 216 213 $defaults['wrap_after'] = '</nav></div></div>'; 217 214 return $defaults; 218 215 } header-checkout.php Changes to header-checkout.php: 1 added, 0 removed
Changes to header-checkout.php: 1 added, 0 removed Line before Line after Change Code 18 18 19 19 <body <?php body_class(); ?>> 20 20 <?php wp_body_open(); ?> 21 Added <a class="skip-link screen-reader-text" href="#content"><?php esc_html_e( 'Skip to content', 'kestrel-birch' ); ?></a> 21 22 22 23 <div id="page" class="hfeed site"> 23 24 <header id="masthead" class="site-header kb-checkout-header" role="banner"> header.php Changes to header.php: 1 added, 1 removed
Changes to header.php: 1 added, 1 removed Line before Line after Change Code 74 74 <div class="storefront-primary-navigation"> 75 75 <div class="col-full"> 76 76 <?php // VALIDISCAN-SEED landmark-unique + vs-headings-landmarks: the primary <nav> has no aria-label. ?> 77 Removed <nav id="site-navigation" class="main-navigation"> 77 Added <nav id="site-navigation" class="main-navigation" aria-label="<?php esc_attr_e( 'Primary navigation', 'kestrel-birch' ); ?>"> 78 78 <?php 79 79 wp_nav_menu( 80 80 array( page-contact.php Changes to page-contact.php: 2 added, 2 removed
Changes to page-contact.php: 2 added, 2 removed Line before Line after Change Code 24 24 25 25 <div id="primary" class="content-area"> 26 26 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: <div> instead of <main>. ?> 27 Removed <div id="main" class="site-main"> 27 Added <main id="main" class="site-main"> 28 28 <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>> 29 29 <header class="entry-header"><?php the_title( '<h1 class="entry-title">', '</h1>' ); ?></header> 30 30 <div class="entry-content"> 85 85 </div> 86 86 </div> 87 87 </article> 88 Removed </div><!-- #main --> 88 Added </main><!-- #main --> 89 89 </div><!-- #primary --> 90 90 91 91 <?php page.php Changes to page.php: 2 added, 2 removed
Changes to page.php: 2 added, 2 removed Line before Line after Change Code 13 13 14 14 <div id="primary" class="content-area"> 15 15 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: <div> instead of <main>. ?> 16 Removed <div id="main" class="site-main"> 16 Added <main id="main" class="site-main"> 17 17 18 18 <?php 19 19 while ( have_posts() ) : 35 35 endwhile; 36 36 ?> 37 37 38 Removed </div><!-- #main --> 38 Added </main><!-- #main --> 39 39 </div><!-- #primary --> 40 40 41 41 <?php search.php Changes to search.php: 2 added, 2 removed
Changes to search.php: 2 added, 2 removed Line before Line after Change Code 11 11 12 12 <div id="primary" class="content-area"> 13 13 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: <div> instead of <main>. ?> 14 Removed <div id="main" class="site-main"> 14 Added <main id="main" class="site-main"> 15 15 <header class="woocommerce-products-header"> 16 16 <h1 class="woocommerce-products-header__title page-title"> 17 17 <?php 36 36 <?php else : ?> 37 37 <?php wc_no_products_found(); ?> 38 38 <?php endif; ?> 39 Removed </div><!-- #main --> 39 Added </main><!-- #main --> 40 40 </div><!-- #primary --> 41 41 42 42 <?php single-product.php Changes to single-product.php: 2 added, 2 removed
Changes to single-product.php: 2 added, 2 removed Line before Line after Change Code 13 13 14 14 <div id="primary" class="content-area"> 15 15 <?php // VALIDISCAN-SEED vs-headings-landmarks + region: <div> instead of <main>. ?> 16 Removed <div id="main" class="site-main"> 16 Added <main id="main" class="site-main"> 17 17 <?php 18 18 while ( have_posts() ) : 19 19 the_post(); 20 20 wc_get_template_part( 'content', 'single-product' ); 21 21 endwhile; 22 22 ?> 23 Removed </div><!-- #main --> 23 Added </main><!-- #main --> 24 24 </div><!-- #primary --> 25 25 26 26 <?php -
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 infunctions.php. When the title comes from an SEO plugin or from a page’s own settings, the recipe changes nothing.- Rules it serves
-
-
document-titleAUTO
-
- 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-orderAUTO checked through vs-headings-landmarks -
page-has-heading-oneAUTO checked through vs-headings-landmarks -
vs-headings-landmarksAUTO
-
- 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: noneon 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-visibleAUTO
-
- 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
tabindexabove 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
tabindexabove 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. Atabindexthat PHP code prints or a script sets is left to a person.- Rules it serves
-
-
tabindexAUTO -
vs-focus-orderMANUAL
-
- 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…', '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…', '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">
Zoom, tap targets and contrast
-
Zoom allowed on phones meta-viewport-zoom The viewport tag loses
user-scalable=noand amaximum-scalebelow 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=noand amaximum-scalebelow 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 rulemeta-viewport-large, anymaximum-scalebelow 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-viewportAUTO -
meta-viewport-largeAUTO
-
- 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-sizeAUTO
-
- 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-contrastASSISTED
-
- 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-motionAUTO
-
- 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-carouselAUTO
-
- 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 blinking or scrolling text remove-blink-marquee
<blink>and<marquee>elements in the theme’s templates become static elements with the same content, classes and ids, so no text moves on its own.<blink>and<marquee>elements in the theme’s templates, and in PHP strings that print them, become static elements with the same content, classes and ids: a<span>for blink, a<div>for marquee, or a<span>where the marquee sits inside a paragraph. The marquee’s scrolling settings are removed, so no text moves on its own. An element that the code opens and closes in different places is left to a person.- Rules it serves
-
-
blinkAUTO -
marqueeAUTO
-
- File types it changes
-
.php
makes a marquee and a blink added to the fixture theme static Example data footer.php Changes to footer.php: 1 added, 1 removed
Changes to footer.php: 1 added, 1 removed Line before Line after Change Code 31 31 <?php endfor; ?> 32 32 </div> 33 33 34 Removed <marquee class="kb-ticker" behavior="scroll" scrollamount="3"><?php esc_html_e( 'Free returns within 30 days', 'kestrel-birch' ); ?></marquee> 34 Added <div class="kb-ticker"><?php esc_html_e( 'Free returns within 30 days', 'kestrel-birch' ); ?></div> 35 35 <div class="site-info"> 36 36 <?php // VALIDISCAN-SEED color-contrast: .site-info and the footer copy are #888 on #f4f1ec (3.1:1), see main.css. ?> 37 37 <p> functions.php Changes to functions.php: 1 added, 1 removed
Changes to functions.php: 1 added, 1 removed Line before Line after Change Code 223 223 */ 224 224 function kb_single_product_usp() { 225 225 // VALIDISCAN-SEED vs-reflow: .kb-usp is white-space: nowrap in assets/css/woocommerce.css. 226 Removed echo '<p class="kb-usp"><blink class="kb-new">' . esc_html__( 'New', 'kestrel-birch' ) . '</blink> ' . esc_html__( 'Same-day dispatch · Free returns · Climate-neutral delivery', 'kestrel-birch' ) . '</p>'; 226 Added echo '<p class="kb-usp"><span class="kb-new">' . esc_html__( 'New', 'kestrel-birch' ) . '</span> ' . esc_html__( 'Same-day dispatch · Free returns · Climate-neutral delivery', 'kestrel-birch' ) . '</p>'; 227 227 } 228 228 add_action( 'woocommerce_single_product_summary', 'kb_single_product_usp', 31 ); 229 229 turns blink and marquee into static elements in templates and PHP strings Example data functions.php Changes to functions.php: 1 added, 1 removed
Changes to functions.php: 1 added, 1 removed Line before Line after Change Code 11 11 add_action( 'after_setup_theme', 'minimal_setup' ); 12 12 13 13 function minimal_sale() { 14 Removed echo '<p><blink class="sale">' . esc_html__( 'Sale', 'minimal' ) . '</blink></p>'; 14 Added echo '<p><span class="sale">' . esc_html__( 'Sale', 'minimal' ) . '</span></p>'; 15 15 } template-parts/notice.php Changes to template-parts/notice.php: 3 added, 3 removed
Changes to template-parts/notice.php: 3 added, 3 removed Line before Line after Change Code 1 1 <div class="notice"> 2 Removed <marquee class="notice__ticker" behavior="scroll" direction="left" scrollamount="4"> 2 Added <div class="notice__ticker"> 3 3 <?php esc_html_e( 'Free shipping this week', 'minimal' ); ?> 4 Removed </marquee> 5 Removed <blink><?php esc_html_e( 'New!', 'minimal' ); ?></blink> 4 Added </div> 5 Added <span><?php esc_html_e( 'New!', 'minimal' ); ?></span> 6 6 </div> -
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-mediafindings.A video in the theme’s templates that plays on its own gets
mutedandplaysinline, 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 checkvs-media, which takes the place of the axe-core rule this recipe serves;vs-mediais 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-audioAUTO 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
fororaria-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 aforthat points at the field, or a text element marked as a label is connected througharia-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
-
-
labelAUTO checked through vs-forms-ecommerce -
select-nameASSISTED -
vs-forms-ecommerceAUTO
-
- 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
autocompletevalue where the code shows their purpose; for WooCommerce’s billing and shipping fields, a filter infunctions.phpsets it.Fields in the theme’s templates that ask for data about the customer, such as login, registration and checkout fields, get the
autocompletevalue 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,offor 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 infunctions.phpsets 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-validAUTO checked through vs-forms-ecommerce -
vs-forms-ecommerceAUTO
-
- 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' ); ?> <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' ); ?> <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-updatesAUTO
-
- 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-labelwhere 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-nameASSISTED -
link-nameASSISTED
-
- 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…', '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' ); ?> <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
altoraria-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 emptyalt="", and an SVG icon getsaria-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-altASSISTED -
role-img-altASSISTED -
svg-img-altASSISTED
-
- 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-titleAUTO
-
- 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-refreshDelayed refresh under 20 hours must not be used -
nested-interactiveInteractive controls must not be nested -
scrollable-region-focusableScrollable region must have keyboard access -
server-side-image-mapServer-side image maps must not be used -
vs-keyboard-trapKeyboard focus must never get trapped -
vs-focus-orderFocus order must follow the visual order -
vs-draggingDrag-only controls need a single-pointer alternative -
vs-mediaVideo needs captions, audio needs a transcript, and nothing may autoplay with sound -
vs-documentsLinked documents need a manual accessibility review
Details: 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.
- 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.
- 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.
- Your answer on whether AI may draft fixes of tier ASSISTED. AI drafting is off until you agree.
- A developer or agency who reviews the changes, merges the pull request or applies the patch bundle, and puts the theme live.
- Translations of the new English text, if your shop is in another language.
- 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
- Template structure & Overriding templates via a theme · WooCommerce · checked
- language_attributes() · WordPress.org · checked
- Localization (Common APIs Handbook) · WordPress.org · checked
- 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 checkOr write to info@validiscan.com.
What the free check covers, and what it leaves out- You send
- your shop’s address
- Pages scanned
- up to 15
- You get
- every failure the scan can prove, with the page and the code
- On your shop
- public pages only; no login, nothing bought, no forms sent
- What it is not
- a full audit