Accessibility fixes in your Shopify theme’s code
Validiscan fixes accessibility failures in your Shopify theme’s Liquid templates, stylesheets, scripts and locale files. The changes arrive as an unpublished copy of your theme, a pull request or a patch bundle, and nothing reaches your live theme until you or your developer publish, merge or apply it.
Last updated:
On this page: 6 answers
Which files in a Shopify theme does Validiscan change?
Validiscan changes your theme’s own files and nothing else: the Liquid files in layout, templates, sections, snippets and blocks, the stylesheets and scripts in assets, the storefront text in locales and the settings in config. A change to any other file is rejected before Validiscan reviews it.
Shopify’s checkout is not part of your theme’s code
A full scan opens the first checkout step, but Shopify no longer supports checkout.liquid, the theme layout file for the checkout, for the information, shipping and payment steps. A failure that a scan finds in those steps therefore cannot be fixed with a change to your theme files.
Details: what a change may add, and what is outside this work
Every change is an ordinary edit to a theme file that you or 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. No fix adds an app or an overlay, and nothing is injected that corrects your pages in the visitor’s browser. Code that is not part of your theme’s files is outside this work, and third-party embeds such as payment or chat widgets are not included in a project.
How does Validiscan get access to your Shopify theme?
Validiscan reaches your Shopify theme in one of three ways: through your GitHub repository, with a Theme Access password, or from a theme file you download yourself. 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, through Validiscan’s GitHub App or an access token | A pull request |
| With a Theme Access password | A Theme Access password and your store’s myshopify.com address | An unpublished copy of your theme |
| From a theme file you download | Your theme’s ZIP file, 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’s code is kept in a GitHub repository, for example through Shopify’s GitHub integration, you give Validiscan access to that repository. You install Validiscan’s GitHub App on it, or give Validiscan an access token. The App asks for read and write access to the repository’s contents and pull requests, and read access to its metadata. The theme’s folders, such as layout, sections and snippets, must be at the top level of the repository, because a change to any other path is rejected. The changes then arrive as a pull request.
With a Theme Access password
Shopify’s free Theme Access app creates passwords that let a developer work on a store’s themes with Shopify CLI. As the store owner, or as a staff member with the Themes permission, you install the app and create a password for Validiscan; Shopify emails Validiscan a link to it, and the password can be viewed only once. You also give Validiscan your store’s myshopify.com address. Validiscan keeps the password encrypted and uses it with Shopify CLI to download your live theme and to upload a new, unpublished copy of it. A Theme Access password allows changes to the store’s themes, so Validiscan’s delivery code refuses any command that would write to the live theme or publish a theme. You end the access by deleting the password in the Theme Access app; as of 6 October 2026, Shopify’s documentation says that deleting a password revokes the access. Validiscan uses the password again after a delivery to check whether the copy has been published, so delete it when the project is over, and create a new one for a later delivery.
From a theme file you download
If neither of the above suits you, you download your theme yourself. The customer portal gives these steps:
- In your Shopify admin, open Online Store, then Themes.
- In the menu of the theme you use, choose “Download theme file”.
- Shopify emails you the ZIP file.
Shopify’s own instructions are on its help page about downloading themes. An admin of your organization in the customer portal, who can be your developer, then uploads the file there, as one ZIP file of up to 100 MB. Validiscan prepares the changes on that copy of your theme and sends them back as a patch bundle.
How do the changes reach your store?
Validiscan delivers the changes in one of three ways: as an unpublished copy of your Shopify theme, as a pull request or as a patch bundle. It delivers only changes it has reviewed and approved, and none of the three ways touches your live theme.
With a Theme Access password, Validiscan downloads your live theme, applies the approved changes and uploads the result as a new, unpublished theme named Validiscan A11y <date>, for example Validiscan A11y 2026-09-24. You get an email when it is ready. Then:
- In the Shopify admin, open Online Store, then Themes, and find the unpublished theme
Validiscan A11y <date>among your draft themes. - Preview it. Your visitors keep seeing the live theme while you do.
- Check the affected pages in the preview, also with the keyboard alone.
- When you are satisfied, choose Publish. Shopify describes this step on its help page about publishing themes.
- Keep your previous theme until you are sure. You can switch back to it.
An unpublished copy of your theme
The copy is your live theme as it was on the day Validiscan downloaded it. If you change the live theme after that day, in its content, settings or code, make the same change in the copy before you publish it, or the change is lost. The customer portal shows whether the copy is waiting to be published, has been published, or was deleted without being published. Shopify’s Help Center on updating themes says that updates are available only for themes installed from the Shopify Theme Store and that automatic updates are not supported for uploaded themes. The copy is uploaded with Shopify CLI, so before you publish it, check under Online Store, then Themes, whether Shopify still offers updates for it. Where fixes go in a Shopify theme, and what a theme update can undo
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. With Shopify’s GitHub integration, a theme follows the branch it is connected to: your developer can connect the pull request’s branch to an unpublished theme to preview it, and merging into the branch your live theme follows changes the live theme.
A patch bundle
From an uploaded theme file, 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, one patch file per fix, the complete changed files and a README.md. Your developer applies the changes, tests them and publishes the theme. What a pull request and a patch bundle contain
What do the recipes change in a Shopify theme?
Each of Validiscan’s 20 recipes for Shopify 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 store’s language html-lang A layout whose
<html>element has a missing, empty or invalidlangattribute gets the language your store is showing.A layout whose
<html>element has nolangattribute, an empty one or an invalid one gets the language your store is showing, so screen readers pronounce the page in the right language. Where the theme already keeps a page’s own language in a metafield, for example for German legal pages in an English store, the layout uses that metafield when it is set. Without such a field the recipe does not guess a page’s language.- Rules it serves
-
-
html-has-langAUTO checked through vs-lang -
html-lang-validAUTO checked through vs-lang -
vs-langAUTO
-
- File types it changes
-
.liquid
Before and after Example data layout/checkout.liquid Changes to layout/checkout.liquid: 1 added, 1 removed
Changes to layout/checkout.liquid: 1 added, 1 removed Line before Line after Change Code 3 3 Checkout layout (the Shopify Plus `checkout.liquid` era). Only the first step 4 4 (contact, delivery, billing) is modelled; nothing is ever submitted. 5 5 {% endcomment %} 6 Removed <html class="no-js"> 6 Added <html class="no-js" lang="{{ request.locale.iso_code }}"> 7 7 <head> 8 8 <meta charset="utf-8"> 9 9 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> layout/theme.liquid Changes to layout/theme.liquid: 3 added, 0 removed
Changes to layout/theme.liquid: 3 added, 0 removed Line before Line after Change Code 6 6 {% endcomment %} 7 7 {%- liquid 8 8 assign page_lang = request.locale.iso_code 9 Added if page.metafields.custom.content_language != blank 10 Added assign page_lang = page.metafields.custom.content_language 11 Added endif 9 12 -%} 10 13 <html class="no-js" lang="{{ page_lang }}"> 11 14 <head> -
Skip link, main area and named menus skip-link-main Depending on what the scan found: a skip link to the main content, a marked main area, and named menus and regions.
A “Skip to content” link becomes the first element of each page. It stays hidden until a keyboard user reaches it, and it leads to the main content, which is marked as the page’s main area. Navigation areas that appear on the same page get distinct names, such as Main menu, Footer menu or Breadcrumbs, and content outside every page area, such as an announcement bar, becomes a named region. An existing skip link that points nowhere is pointed at the main content. Which of these changes are made depends on what the scan found.
- Rules it serves
-
-
bypassAUTO checked through vs-headings-landmarks -
skip-linkAUTO -
landmark-one-mainAUTO checked through vs-headings-landmarks -
regionAUTO -
vs-headings-landmarksAUTO -
landmark-uniqueASSISTED
-
- File types it changes
-
.css.liquid.json.liquid
Before and after Example data assets/base.css.liquid Changes to assets/base.css.liquid: 30 added, 0 removed
Changes to assets/base.css.liquid: 30 added, 0 removed Line before Line after Change Code 1866 1866 } 1867 1867 1868 1868 /* ----------------------------------------------------------------- motion */ 1869 Added 1870 Added /* validiscan:skip-link-main */ 1871 Added .skip-to-content-link:not(:focus) { 1872 Added position: absolute !important; 1873 Added overflow: hidden; 1874 Added width: 1px; 1875 Added height: 1px; 1876 Added margin: -1px; 1877 Added padding: 0; 1878 Added border: 0; 1879 Added clip: rect(0 0 0 0); 1880 Added white-space: nowrap; 1881 Added } 1882 Added 1883 Added .skip-to-content-link:focus { 1884 Added z-index: 9999; 1885 Added position: fixed !important; 1886 Added top: 0.5rem; 1887 Added left: 0.5rem; 1888 Added overflow: visible; 1889 Added width: auto; 1890 Added height: auto; 1891 Added margin: 0; 1892 Added padding: 0.75rem 1.25rem; 1893 Added clip: auto; 1894 Added background-color: #ffffff; 1895 Added color: #121212; 1896 Added text-decoration: underline; 1897 Added } 1898 Added /* /validiscan:skip-link-main */ layout/checkout.liquid Changes to layout/checkout.liquid: 4 added, 3 removed
Changes to layout/checkout.liquid: 4 added, 3 removed Line before Line after Change Code 18 18 </head> 19 19 20 20 <body class="template-checkout"> 21 Added <a class="skip-to-content-link" href="#MainContent">{{ 'accessibility.skip_to_text' | t }}</a> 21 22 <header class="checkout-header"> 22 23 <div class="page-width checkout-header__inner"> 23 24 <a href="{{ routes.root_url }}" class="checkout-header__logo"><img src="{{ 'logo.svg' | asset_url }}" width="140" height="32"></a> 24 Removed <nav class="checkout-breadcrumb"> 25 Added <nav class="checkout-breadcrumb" aria-label="{{ 'accessibility.breadcrumbs' | t | escape }}"> 25 26 <ol class="checkout-breadcrumb__list" role="list"> 26 27 <li><a href="{{ routes.cart_url }}" class="checkout-breadcrumb__link">Cart</a></li> 27 28 <li><span class="checkout-breadcrumb__current">Information</span></li> 31 32 </nav> 32 33 </div> 33 34 </header> 34 Removed <div class="main checkout" id="MainContent"> 35 Added <main class="main checkout" id="MainContent" tabindex="-1"> 35 36 {{ content_for_layout }} 36 Removed </div> 37 Added </main> 37 38 38 39 <footer class="checkout-footer"> 39 40 <div class="page-width"> layout/theme.focused.liquid Changes to layout/theme.focused.liquid: 1 added, 0 removed
Changes to layout/theme.focused.liquid: 1 added, 0 removed Line before Line after Change Code 22 22 </head> 23 23 24 24 <body class="gradient template-{{ template.name | handleize }} layout-focused"> 25 Added <a class="skip-to-content-link" href="#MainContent">{{ 'accessibility.skip_to_text' | t }}</a> 25 26 {% section 'header' %} 26 27 27 28 <main id="MainContent" class="content-for-layout focus-none" role="main" tabindex="-1"> layout/theme.liquid Changes to layout/theme.liquid: 1 added, 0 removed
Changes to layout/theme.liquid: 1 added, 0 removed Line before Line after Change Code 28 28 </head> 29 29 30 30 <body class="gradient template-{{ template.name | handleize }}"> 31 Added <a class="skip-to-content-link" href="#MainContent">{{ 'accessibility.skip_to_text' | t }}</a> 31 32 32 33 {% sections 'header-group' %} 33 34 locales/en.default.json Changes to locales/en.default.json: 3 added, 1 removed
Changes to locales/en.default.json: 3 added, 1 removed Line before Line after Change Code 34 34 "total_reviews": "{{ count }} reviews", 35 35 "link_messages": { 36 36 "new_window": "Opens in a new window." 37 Removed } 37 Added }, 38 Added "breadcrumbs": "Breadcrumbs", 39 Added "mobile_menu": "Mobile menu" 38 40 }, 39 41 "sections": { 40 42 "header": { sections/footer.liquid Changes to sections/footer.liquid: 1 added, 1 removed
Changes to sections/footer.liquid: 1 added, 1 removed Line before Line after Change Code 17 17 {%- endform -%} 18 18 </div> 19 19 20 Removed <nav class="footer__menus"> 20 Added <nav class="footer__menus" aria-label="{{ 'sections.footer.menu' | t | escape }}"> 21 21 {%- for handle in menu_handles %} 22 22 {%- assign footer_menu = linklists[handle] %} 23 23 <div class="footer-block footer-block--menu"> sections/header.liquid Changes to sections/header.liquid: 2 added, 2 removed
Changes to sections/header.liquid: 2 added, 2 removed Line before Line after Change Code 11 11 {%- render 'icon', icon: 'menu', size: icon_size -%} 12 12 </summary> 13 13 <div class="menu-drawer"> 14 Removed <nav class="menu-drawer__navigation"> 14 Added <nav class="menu-drawer__navigation" aria-label="{{ 'accessibility.mobile_menu' | t | escape }}"> 15 15 <ul class="menu-drawer__menu list-unstyled" role="list"> 16 16 {%- for link in menu.links %} 17 17 <li><a href="{{ link.url }}" class="menu-drawer__menu-item">{{ link.title | escape }}</a></li> 25 25 <a href="{{ routes.root_url }}" class="header__heading-link"><img src="{{ 'logo.svg' | asset_url }}" width="{{ settings.logo_width }}" height="32" class="header__heading-logo"></a> 26 26 {% if logo_is_heading %}</h1>{% else %}</div>{% endif %} 27 27 28 Removed <nav class="header__inline-menu"> 28 Added <nav class="header__inline-menu" aria-label="{{ 'sections.header.main_menu' | t | escape }}"> 29 29 <ul class="list-menu list-menu--inline" role="list"> 30 30 {%- for link in menu.links %} 31 31 <li> -
A title on every page document-title-template Layouts whose
<title>is missing, empty or the same fixed text on every page get a title built from Shopify’s variables.Every layout whose
<title>is missing, empty or the same fixed text on every page gets a title built from Shopify’s variables. If your main layout already has a title that names the page, the other layouts get the same one. Otherwise the title holds the page title, the page number of a paginated list and the shop name, with the word for “page” translated through the theme’s locale files.- Rules it serves
-
-
document-titleAUTO
-
- File types it changes
-
.liquid
Before and after Example data layout/theme.focused.liquid Changes to layout/theme.focused.liquid: 1 added, 1 removed
Changes to layout/theme.focused.liquid: 1 added, 1 removed Line before Line after Change Code 11 11 <meta charset="utf-8"> 12 12 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 13 13 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> 14 Removed <title></title> 14 Added <title>{{ page_title }}{% unless page_title contains shop.name %} – {{ shop.name }}{% endunless %}</title> 15 15 <link rel="canonical" href="{{ canonical_url }}"> 16 16 <!-- Theme assets: //cdn.shopify.com/s/files/1/0712/3456/7890/t/12/assets/ (served locally by the fixture server) --> 17 17 {% render 'meta-tags' %} -
Headings without skipped levels heading-levels A heading that skips a level gets the next level below the heading before it; the recipe adds no hidden heading.
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 section file where it is written. Where the theme styles headings by their level, a small style rule keeps the heading’s old look. A page without a main heading gets the element that already looks like its title turned into the h1; on the home page that is the theme’s own option to show the logo as the h1. If a page has no such element, the recipe adds no hidden heading, because that is a content decision.
- 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
-
.liquid
Before and after Example data sections/featured-collection.liquid Changes to sections/featured-collection.liquid: 1 added, 1 removed
Changes to sections/featured-collection.liquid: 1 added, 1 removed Line before Line after Change Code 1 1 {%- liquid 2 2 assign featured = section.settings.collection 3 Removed assign card_heading = 'h4' 3 Added assign card_heading = 'h3' 4 4 -%} 5 5 <div class="featured-collection page-width section-spacing"> 6 6 <div class="title-wrapper"> sections/header.liquid Changes to sections/header.liquid: 3 added, 0 removed
Changes to sections/header.liquid: 3 added, 0 removed Line before Line after Change Code 3 3 assign menu = linklists[section.settings.menu] 4 4 assign icon_size = 18 5 5 assign logo_is_heading = false 6 Added if request.page_type == 'index' 7 Added assign logo_is_heading = true 8 Added endif 6 9 -%} 7 10 <div class="header-wrapper"> 8 11 <header class="header page-width"{% if header_lang != '' %} lang="{{ header_lang }}"{% endif %}> sections/main-collection.liquid Changes to sections/main-collection.liquid: 1 added, 1 removed
Changes to sections/main-collection.liquid: 1 added, 1 removed Line before Line after Change Code 6 6 wishlist heart nested inside the product image link. 7 7 {%- endcomment -%} 8 8 {%- liquid 9 Removed assign card_heading = 'h4' 9 Added assign card_heading = 'h3' 10 10 assign price_max = collection.price_max_rounded 11 11 assign in_stock_count = collection.products | where: 'available', true | size 12 12 assign out_of_stock_count = collection.products_count | minus: in_stock_count sections/main-product.liquid Changes to sections/main-product.liquid: 1 added, 1 removed
Changes to sections/main-product.liquid: 1 added, 1 removed Line before Line after Change Code 13 13 assign featured = product.featured_image 14 14 assign form_id = 'product-form-' | append: section.id 15 15 assign qty_id = 'Quantity-' | append: section.id 16 Removed assign sub_heading = 'h4' 16 Added assign sub_heading = 'h2' 17 17 assign size_option = nil 18 18 for option in product.options_with_values 19 19 if option.name == 'Size' sections/main-search.liquid Changes to sections/main-search.liquid: 1 added, 1 removed
Changes to sections/main-search.liquid: 1 added, 1 removed Line before Line after Change Code 1 1 {%- liquid 2 Removed assign card_heading = 'h4' 2 Added assign card_heading = 'h3' 3 3 -%} 4 4 <div class="template-search page-width"> 5 5 <h1 class="template-search__title">{{ 'templates.search.title' | t }}</h1>
Keyboard use and focus
-
A visible focus outline focus-visible-styles One outline for keyboard focus, in your theme’s accent colour or a fallback, at a contrast of at least 3:1 against the page background.
The main stylesheet gets one outline for keyboard focus: 2 pixels, solid, 2 pixels away from the element, in your theme’s accent colour (or its text colour, or black or white, where the theme has no accent colour), made lighter or darker in the same hue until it has a contrast of at least 3:1 against the page background. Theme rules that would still hide the outline lose their
outline: none, unless they show focus in another way, such as a border or a shadow. The outline has one colour for the whole theme, so on sections with a dark colour scheme it may need a lighter colour, which your developer checks in the preview.- Rules it serves
-
-
vs-focus-visibleAUTO
-
- File types it changes
-
.css.liquid
Before and after Example data assets/base.css.liquid Changes to assets/base.css.liquid: 7 added, 0 removed
Changes to assets/base.css.liquid: 7 added, 0 removed Line before Line after Change Code 1866 1866 } 1867 1867 1868 1868 /* ----------------------------------------------------------------- motion */ 1869 Added 1870 Added /* validiscan:focus-visible-styles */ 1871 Added :focus-visible { 1872 Added outline: 2px solid #1f4d3a; 1873 Added outline-offset: 2px; 1874 Added } 1875 Added /* /validiscan:focus-visible-styles */ -
A tab order that follows the page remove-positive-tabindex A
tabindexabove 0 is removed or set to 0, so the Tab key follows the order of the page, unless the theme’s template code writes it.A
tabindexabove 0 pulls an element to the front of the keyboard order. The recipe removes it from links, buttons, form fields and other elements that 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. Atabindexthat the theme’s template code writes is left to a person. The recipe also handles the findings of the focus order checkvs-focus-orderthat are about atabindexabove 0; the rest of that check stays MANUAL.- Rules it serves
-
-
tabindexAUTO -
vs-focus-orderMANUAL
-
- File types it changes
-
.liquid
Before and after Example data sections/header.liquid Changes to sections/header.liquid: 1 added, 1 removed
Changes to sections/header.liquid: 1 added, 1 removed Line before Line after Change Code 37 37 38 38 <div class="header__search"> 39 39 <form action="{{ routes.search_url }}" method="get" role="search" class="search"> 40 Removed <input class="search__input field__input" type="search" name="q" value="" placeholder="{{ 'general.search.placeholder' | t }}" tabindex="1"> 40 Added <input class="search__input field__input" type="search" name="q" value="" placeholder="{{ 'general.search.placeholder' | t }}"> 41 41 <button type="submit" class="search__button"> 42 42 {%- render 'icon', icon: 'search', size: 20 -%} 43 43 </button>
Zoom, tap targets and contrast
-
Zoom allowed on phones meta-viewport-zoom
user-scalable=noand anymaximum-scalebelow 5 are removed from the viewport tags, so visitors can zoom up to 500 %, unless the theme’s template code sets the limit.The recipe removes
user-scalable=noand anymaximum-scalebelow 5 from the viewport tags in the theme’s Liquid files, so visitors can pinch-zoom your store up to 500 %. The other viewport settings stay exactly as they were. A zoom limit that the theme’s template code sets is left to a person.- Rules it serves
-
-
meta-viewportAUTO -
meta-viewport-largeAUTO
-
- File types it changes
-
.liquid
Before and after Example data layout/checkout.liquid Changes to layout/checkout.liquid: 1 added, 1 removed
Changes to layout/checkout.liquid: 1 added, 1 removed Line before Line after Change Code 6 6 <html class="no-js"> 7 7 <head> 8 8 <meta charset="utf-8"> 9 Removed <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> 9 Added <meta name="viewport" content="width=device-width,initial-scale=1"> 10 10 <meta name="robots" content="noindex"> 11 11 <link rel="canonical" href="{{ canonical_url }}"> 12 12 <!-- Checkout assets: //cdn.shopify.com/s/files/1/0712/3456/7890/t/12/assets/ (served locally by the fixture server) --> layout/theme.focused.liquid Changes to layout/theme.focused.liquid: 1 added, 1 removed
Changes to layout/theme.focused.liquid: 1 added, 1 removed Line before Line after Change Code 10 10 <head> 11 11 <meta charset="utf-8"> 12 12 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 13 Removed <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> 13 Added <meta name="viewport" content="width=device-width,initial-scale=1"> 14 14 <title></title> 15 15 <link rel="canonical" href="{{ canonical_url }}"> 16 16 <!-- Theme assets: //cdn.shopify.com/s/files/1/0712/3456/7890/t/12/assets/ (served locally by the fixture server) --> layout/theme.liquid Changes to layout/theme.liquid: 1 added, 1 removed
Changes to layout/theme.liquid: 1 added, 1 removed Line before Line after Change Code 11 11 <head> 12 12 <meta charset="utf-8"> 13 13 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 14 Removed <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> 14 Added <meta name="viewport" content="width=device-width,initial-scale=1"> 15 15 <meta name="theme-color" content="{{ settings.color_background }}"> 16 16 <link rel="canonical" href="{{ canonical_url }}"> 17 17 {%- comment -%} On the live store theme assets load from //cdn.shopify.com/s/files/1/0712/3456/7890/t/12/assets/; the fixture serves them from {{ routes.root_url }}assets/. {%- endcomment %} -
Larger tap targets target-size-padding Controls the scan found too small, such as header icons or colour swatches, get a minimum size of 24 by 24 pixels.
Controls that the scan found too small, such as header icons, colour swatches or quantity buttons, get a minimum size of 24 by 24 pixels in the main stylesheet, addressed by their class. The icons inside them keep their size. A link or other element that the theme leaves inline is made
inline-flexwith its icon centred, because inline elements ignore a minimum size.- Rules it serves
-
-
vs-target-sizeAUTO
-
- File types it changes
-
.css.liquid
Before and after Example data assets/base.css.liquid Changes to assets/base.css.liquid: 10 added, 0 removed
Changes to assets/base.css.liquid: 10 added, 0 removed Line before Line after Change Code 1866 1866 } 1867 1867 1868 1868 /* ----------------------------------------------------------------- motion */ 1869 Added 1870 Added /* validiscan:target-size-padding */ 1871 Added .header__icon--account, 1872 Added .header__icon--cart, 1873 Added .swatch--button, 1874 Added .quantity__button { 1875 Added min-width: 24px; 1876 Added min-height: 24px; 1877 Added } 1878 Added /* /validiscan:target-size-padding */ -
Text colours with enough contrast contrast-tokens A failing text colour defined in one place, and not shared, moves to the nearest shade of its hue with the required contrast.
When text fails the contrast check and its colour is defined in a single place, that colour moves to the nearest lighter or darker shade of the same hue that reaches the required contrast against every background the scan reported for it. The place can be the stylesheet rule that styles the text, or a CSS variable or a colour setting in
config/that nothing else in the theme uses. A colour that is computed in Liquid or defined more than once is left to a person, and so is a shared colour, because changing it would affect colour pairs the scan did not measure.settings_data.json - Rules it serves
-
-
color-contrastASSISTED
-
- File types it changes
-
.css.liquid
Before and after Example data assets/base.css.liquid Changes to assets/base.css.liquid: 3 added, 3 removed
Changes to assets/base.css.liquid: 3 added, 3 removed Line before Line after Change Code 745 745 font-size: 0.8125rem; 746 746 letter-spacing: 0.04em; 747 747 text-transform: uppercase; 748 Removed color: #929292; 748 Added color: #767676; 749 749 } 750 750 751 751 .rating { 1013 1013 1014 1014 .copyright__content { 1015 1015 margin: 0; 1016 Removed color: #929292; 1016 Added color: #6f6f6f; 1017 1017 } 1018 1018 1019 1019 .policies { 1077 1077 1078 1078 .cookie-banner__text, 1079 1079 .cookie-banner__link { 1080 Removed color: #9a9a9a; 1080 Added color: #767676; 1081 1081 } 1082 1082 1083 1083 .cookie-banner__icon-button {
Motion, slideshows and sound
-
Less motion when a visitor asks for it reduced-motion-guard For visitors whose device asks for reduced motion, CSS animations and transitions are cut short, smooth scrolling is off, and a slideshow’s autoplay that the theme’s script starts is skipped.
When a visitor’s device is set to reduce motion, a block at the end of the main stylesheet lets every CSS animation run only once, too briefly to see, shortens transitions and turns smooth scrolling off. Where the theme’s script starts a slideshow’s autoplay itself, the start is skipped for those visitors. Motion that a third-party script drives is not changed by this recipe.
- Rules it serves
-
-
vs-motionAUTO
-
- File types it changes
-
.css.liquid
Before and after Example data assets/base.css.liquid Changes to assets/base.css.liquid: 13 added, 0 removed
Changes to assets/base.css.liquid: 13 added, 0 removed Line before Line after Change Code 1866 1866 } 1867 1867 1868 1868 /* ----------------------------------------------------------------- motion */ 1869 Added 1870 Added /* validiscan:reduced-motion-guard */ 1871 Added @media (prefers-reduced-motion: reduce) { 1872 Added *, 1873 Added *::before, 1874 Added *::after { 1875 Added animation-duration: 0.01ms !important; 1876 Added animation-iteration-count: 1 !important; 1877 Added transition-duration: 0.01ms !important; 1878 Added scroll-behavior: auto !important; 1879 Added } 1880 Added } 1881 Added /* /validiscan:reduced-motion-guard */ -
A pause button for moving slideshows slider-pause-control A slideshow that moves on its own gets a visible, translated pause and play button connected to its own autoplay.
A slideshow that moves on its own gets a visible, translated button to pause and play it, among its controls and shown only when autoplay is on. A short script in the theme’s main script file connects the button to the slider’s own autoplay: Dawn’s slideshow, Swiper, Slick, Flickity, Splide, or a CSS animation. For visitors whose device asks for reduced motion, the slideshow starts paused.
- Rules it serves
-
-
vs-carouselAUTO
-
- File types it changes
-
.css.liquid.js.json.liquid
Before and after Example data assets/base.css.liquid Changes to assets/base.css.liquid: 21 added, 0 removed
Changes to assets/base.css.liquid: 21 added, 0 removed Line before Line after Change Code 1866 1866 } 1867 1867 1868 1868 /* ----------------------------------------------------------------- motion */ 1869 Added 1870 Added /* validiscan:slider-pause-control */ 1871 Added [data-autoplay-paused], 1872 Added [data-autoplay-paused] * { 1873 Added animation-play-state: paused !important; 1874 Added } 1875 Added 1876 Added .slider-pause { 1877 Added display: inline-flex; 1878 Added align-items: center; 1879 Added justify-content: center; 1880 Added min-width: 2.75rem; 1881 Added min-height: 2.75rem; 1882 Added padding: 0.5rem 1rem; 1883 Added border: 1px solid currentColor; 1884 Added background: transparent; 1885 Added color: inherit; 1886 Added font: inherit; 1887 Added cursor: pointer; 1888 Added } 1889 Added /* /validiscan:slider-pause-control */ assets/global.js Changes to assets/global.js: 53 added, 0 removed
Changes to assets/global.js: 53 added, 0 removed Line before Line after Change Code 412 412 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); 413 413 else boot(); 414 414 })(); 415 Added 416 Added /* validiscan:slider-pause-control */ 417 Added (function () { 418 Added 'use strict'; 419 Added var ROOTS = 'slideshow-component, slider-component, .swiper, .swiper-container, .slick-slider, .flickity-enabled, .splide, [data-autoplay]'; 420 Added var reduce = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; 421 Added 422 Added function rootOf(button) { 423 Added return button.closest(ROOTS) || button.previousElementSibling; 424 Added } 425 Added 426 Added function setPaused(button, paused) { 427 Added var root = rootOf(button); 428 Added if (!root) return; 429 Added if (paused) root.setAttribute('data-autoplay-paused', ''); 430 Added else root.removeAttribute('data-autoplay-paused'); 431 Added if (root.swiper && root.swiper.autoplay) { 432 Added if (paused) root.swiper.autoplay.stop(); 433 Added else root.swiper.autoplay.start(); 434 Added } 435 Added var $ = window.jQuery; 436 Added if ($ && $(root).hasClass('slick-initialized')) $(root).slick(paused ? 'slickPause' : 'slickPlay'); 437 Added var flickity = window.Flickity && window.Flickity.data(root); 438 Added if (flickity) { 439 Added if (paused) flickity.pausePlayer(); 440 Added else flickity.unpausePlayer(); 441 Added } 442 Added var autoplay = root.splide && root.splide.Components && root.splide.Components.Autoplay; 443 Added if (autoplay) { 444 Added if (paused) autoplay.pause(); 445 Added else autoplay.play(); 446 Added } 447 Added if (root.localName.indexOf('-') > 0 && typeof root.pause === 'function' && typeof root.play === 'function') { 448 Added if (paused) root.pause(); 449 Added else root.play(); 450 Added } 451 Added button.setAttribute('data-paused', paused ? 'true' : 'false'); 452 Added button.textContent = button.getAttribute(paused ? 'data-label-play' : 'data-label-pause') || button.textContent; 453 Added } 454 Added 455 Added function init() { 456 Added Array.prototype.forEach.call(document.querySelectorAll('[data-slider-pause]'), function (button) { 457 Added button.addEventListener('click', function () { 458 Added setPaused(button, button.getAttribute('data-paused') !== 'true'); 459 Added }); 460 Added if (reduce && reduce.matches) setPaused(button, true); 461 Added }); 462 Added } 463 Added 464 Added if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); 465 Added else init(); 466 Added })(); 467 Added /* /validiscan:slider-pause-control */ locales/en.default.json Changes to locales/en.default.json: 3 added, 1 removed
Changes to locales/en.default.json: 3 added, 1 removed Line before Line after Change Code 34 34 "total_reviews": "{{ count }} reviews", 35 35 "link_messages": { 36 36 "new_window": "Opens in a new window." 37 Removed } 37 Added }, 38 Added "pause_slideshow": "Pause slideshow", 39 Added "play_slideshow": "Play slideshow" 38 40 }, 39 41 "sections": { 40 42 "header": { sections/slideshow.liquid Changes to sections/slideshow.liquid: 3 added, 0 removed
Changes to sections/slideshow.liquid: 3 added, 0 removed Line before Line after Change Code 68 68 <button type="button" class="slider-button slider-button--next" name="next" data-slide-next> 69 69 {%- render 'icon', icon: 'chevron-right', size: 20 -%} 70 70 </button> 71 Added {%- if autoplay %} 72 Added <button type="button" class="slider-pause" data-slider-pause data-label-pause="{{ 'accessibility.pause_slideshow' | t | escape }}" data-label-play="{{ 'accessibility.play_slideshow' | t | escape }}">{{ 'accessibility.pause_slideshow' | t }}</button> 73 Added {%- endif %} 71 74 </div> 72 75 </slideshow-component> 73 76 -
No blinking or scrolling text remove-blink-marquee
<blink>and<marquee>elements become static elements with the same content, classes and ids, so no text moves on its own.<blink>and<marquee>elements in the theme become static elements with the same content, classes and ids: a<span>for blink, a<div>for marquee, or a<span>where the marquee sits inside a paragraph or a link. The marquee’s scrolling settings are removed, so no text moves on its own.- Rules it serves
-
-
blinkAUTO -
marqueeAUTO
-
- File types it changes
-
.liquid
Before and after Example data sections/announcement-bar.liquid Changes to sections/announcement-bar.liquid: 2 added, 2 removed
Changes to sections/announcement-bar.liquid: 2 added, 2 removed Line before Line after Change Code 3 3 -%} 4 4 <div class="announcement-bar color-accent"{% if bar_lang != '' %} lang="{{ bar_lang }}"{% endif %}> 5 5 <div class="page-width"> 6 Removed <marquee class="announcement-bar__message" behavior="scroll" direction="left" scrollamount="4">{{ section.settings.text | escape }}</marquee> 7 Removed <p class="announcement-bar__note"><blink class="announcement-bar__flag">New</blink> winter collection</p> 6 Added <div class="announcement-bar__message">{{ section.settings.text | escape }}</div> 7 Added <p class="announcement-bar__note"><span class="announcement-bar__flag">New</span> winter collection</p> 8 8 </div> 9 9 </div> 10 10 -
No sound that starts by itself mute-autoplay Video that plays on its own is muted and audio no longer starts by itself, except in embedded YouTube and Vimeo players; on Shopify the recipe is not used for
vs-mediafindings.A video that plays on its own gets
mutedandplaysinline, so it keeps playing silently, as a background video does, and Shopify’svideo_tagfilter getsmuted: true. Audio that starts by itself no longer does. Embedded YouTube and Vimeo players are left to a person. In a Validiscan scan, sound that starts by itself is reported by the media checkvs-media, which takes the place of the axe-core rule this recipe serves.vs-mediahas the tier MANUAL, and on Shopify this recipe is not used for its findings.- Rules it serves
-
-
no-autoplay-audioAUTO checked through vs-media
-
- File types it changes
-
.liquid
Before and after Example data sections/announcement-bar.liquid Changes to sections/announcement-bar.liquid: 2 added, 2 removed
Changes to sections/announcement-bar.liquid: 2 added, 2 removed Line before Line after Change Code 3 3 -%} 4 4 <div class="announcement-bar color-accent"{% if bar_lang != '' %} lang="{{ bar_lang }}"{% endif %}> 5 5 <div class="page-width"> 6 Removed <audio class="announcement-bar__chime" src="{{ 'chime.wav' | asset_url }}" autoplay></audio> 7 Removed <video class="announcement-bar__clip" src="{{ 'chime.wav' | asset_url }}" autoplay loop width="16" height="16"></video> 6 Added <audio class="announcement-bar__chime" src="{{ 'chime.wav' | asset_url }}"></audio> 7 Added <video class="announcement-bar__clip" src="{{ 'chime.wav' | asset_url }}" autoplay loop width="16" height="16" muted playsinline></video> 8 8 <p class="announcement-bar__message">{{ section.settings.text | escape }}</p> 9 9 </div> 10 10 </div>
Forms, prices and the cart
-
Labels connected to their fields label-association A label already visible on the page but not connected to its form field gets connected, without changing the layout.
A form field whose label text is already visible on the page, but not connected to the field, gets connected: a
<label>next to it gets aforthat points at the field, or a text just before the field, such as the name of a size or colour option, becomes its label througharia-labelledby, so the layout does not change. Screen readers then read the label when the field gets focus. A field that only has a placeholder is left to a person, because writing a label is a content decision.- Rules it serves
-
-
labelAUTO checked through vs-forms-ecommerce -
select-nameASSISTED -
vs-forms-ecommerceAUTO
-
- File types it changes
-
.liquid
Before and after Example data sections/main-collection.liquid Changes to sections/main-collection.liquid: 2 added, 2 removed
Changes to sections/main-collection.liquid: 2 added, 2 removed Line before Line after Change Code 64 64 <div class="collection__products"> 65 65 <div class="collection__toolbar"> 66 66 <div class="collection__sort"> 67 Removed <label class="collection__sort-label">Sort by</label> 68 Removed <select name="sort_by" class="select__select" form="FacetFiltersForm"> 67 Added <label class="collection__sort-label" for="SortBy-{{ section.id }}">Sort by</label> 68 Added <select name="sort_by" class="select__select" form="FacetFiltersForm" id="SortBy-{{ section.id }}"> 69 69 <option value="manual" selected>Featured</option> 70 70 <option value="best-selling">Best selling</option> 71 71 <option value="price-ascending">Price, low to high</option> sections/main-product.liquid Changes to sections/main-product.liquid: 2 added, 2 removed
Changes to sections/main-product.liquid: 2 added, 2 removed Line before Line after Change Code 94 94 </div> 95 95 {%- else %} 96 96 <div class="product-form__input product-form__input--dropdown"> 97 Removed <p class="form__label">{{ option.name | escape }}</p> 97 Added <p class="form__label" id="Options-{{ section.id }}-{{ forloop.index0 }}-label">{{ option.name | escape }}</p> 98 98 <div class="select"> 99 Removed <select class="select__select" name="options[{{ option.name | escape }}]" data-option-index="{{ option_index }}"> 99 Added <select class="select__select" name="options[{{ option.name | escape }}]" data-option-index="{{ option_index }}" aria-labelledby="Options-{{ section.id }}-{{ forloop.index0 }}-label"> 100 100 {%- for value in option.values %} 101 101 <option value="{{ value | escape }}"{% if value == option.selected_value %} selected{% endif %}>{{ value | escape }}</option> 102 102 {%- endfor %} -
Address, account and login fields that browsers can fill in autocomplete-tokens Address, account, login and contact fields the scan flagged get the
autocompletevalue for what they ask for, where the code shows it.Address, account, login and contact fields that the scan flagged get the
autocompletevalue for what they ask for, such as email, first and last name, street address, postal code, country, phone number or password. The value is worked out from the field’s type, name, label and placeholder. Where a snippet prints its fields from parameters, the fields print anautocompleteparameter, and eachrendercall of the snippet passes the value its own arguments call for. Browsers can then fill the fields in, and assistive technology can tell what each one is for. A field whose purpose the code does not show is left as it is.- Rules it serves
-
-
autocomplete-validAUTO checked through vs-forms-ecommerce -
vs-forms-ecommerceAUTO
-
- File types it changes
-
.liquid
Before and after Example data sections/contact-form.liquid Changes to sections/contact-form.liquid: 1 added, 1 removed
Changes to sections/contact-form.liquid: 1 added, 1 removed Line before Line after Change Code 14 14 {%- form 'contact', id: 'ContactForm', class: 'contact__form' %} 15 15 <div class="contact__fields"> 16 16 <div class="field"> 17 Removed <input class="field__input" type="text" name="contact[name]" placeholder="{{ 'templates.contact.form.name' | t }}"> 17 Added <input class="field__input" type="text" name="contact[name]" placeholder="{{ 'templates.contact.form.name' | t }}" autocomplete="name"> 18 18 </div> 19 19 <div class="field"> 20 20 <input class="field__input" type="email" name="contact[email]" placeholder="{{ 'templates.contact.form.email' | t }} *" required> sections/footer.liquid Changes to sections/footer.liquid: 1 added, 1 removed
Changes to sections/footer.liquid: 1 added, 1 removed Line before Line after Change Code 11 11 {%- form 'customer', id: 'ContactFooter', class: 'footer__newsletter newsletter-form' -%} 12 12 <input type="hidden" name="contact[tags]" value="newsletter"> 13 13 <div class="newsletter-form__field-wrapper"> 14 Removed <input type="email" name="contact[email]" class="field__input" placeholder="{{ 'newsletter.label' | t }}" autocorrect="off" autocapitalize="off"> 14 Added <input type="email" name="contact[email]" class="field__input" placeholder="{{ 'newsletter.label' | t }}" autocorrect="off" autocapitalize="off" autocomplete="email"> 15 15 <button type="submit" class="button newsletter-form__button" name="commit">{{ 'newsletter.button' | t }}</button> 16 16 </div> 17 17 {%- endform -%} sections/main-login.liquid Changes to sections/main-login.liquid: 1 added, 1 removed
Changes to sections/main-login.liquid: 1 added, 1 removed Line before Line after Change Code 10 10 {%- form 'customer_login', novalidate: 'novalidate' %} 11 11 <div class="field"> 12 12 <label class="field__label" for="CustomerEmail">{{ 'customer.login_page.email' | t }}</label> 13 Removed <input class="field__input" type="email" name="customer[email]" id="CustomerEmail" autocorrect="off" autocapitalize="off"> 13 Added <input class="field__input" type="email" name="customer[email]" id="CustomerEmail" autocorrect="off" autocapitalize="off" autocomplete="email"> 14 14 </div> 15 15 <div class="field"> 16 16 <label class="field__label" for="CustomerPassword">{{ 'customer.login_page.password' | t }}</label> snippets/checkout-field.liquid Changes to snippets/checkout-field.liquid: 4 added, 4 removed
Changes to snippets/checkout-field.liquid: 4 added, 4 removed Line before Line after Change Code 15 15 <div class="field"> 16 16 {%- case legacy %} 17 17 {%- when 'placeholder' %} 18 Removed <input class="field__input" type="{{ field_type }}" id="{{ field_id }}" name="{{ field_name }}" placeholder="{{ label }}"{% if required %} required{% endif %}> 18 Added <input class="field__input" type="{{ field_type }}" id="{{ field_id }}" name="{{ field_name }}" placeholder="{{ label }}"{% if required %} required{% endif %} autocomplete="{{ autocomplete }}"> 19 19 {%- when 'starred' %} 20 20 <label class="field__label" for="{{ field_id }}">{{ label }} *</label> 21 Removed <input class="field__input" type="{{ field_type }}" id="{{ field_id }}" name="{{ field_name }}"> 21 Added <input class="field__input" type="{{ field_type }}" id="{{ field_id }}" name="{{ field_name }}" autocomplete="{{ autocomplete }}"> 22 22 {%- when 'off' %} 23 23 <label class="field__label" for="{{ field_id }}">{{ label }}</label> 24 Removed <input class="field__input" type="{{ field_type }}" id="{{ field_id }}" name="{{ field_name }}" autocomplete="off"{% if required %} required{% endif %}> 24 Added <input class="field__input" type="{{ field_type }}" id="{{ field_id }}" name="{{ field_name }}" autocomplete="{{ autocomplete }}"{% if required %} required{% endif %}> 25 25 {%- else %} 26 26 <label class="field__label" for="{{ field_id }}">{{ label }}</label> 27 Removed <input class="field__input" type="{{ field_type }}" id="{{ field_id }}" name="{{ field_name }}"{% if required %} required{% endif %}> 27 Added <input class="field__input" type="{{ field_type }}" id="{{ field_id }}" name="{{ field_name }}"{% if required %} required{% endif %} autocomplete="{{ autocomplete }}"> 28 28 {%- endcase %} 29 29 </div> -
Price, stock and cart changes announced live-region-cart-variant The price, stock note or cart subtotal that the theme’s script rewrites becomes a status region that screen readers announce, unless the area also holds buttons or links.
The part of a page that the theme’s script rewrites when a customer picks another variant or changes a quantity, such as the price, the stock note or the cart subtotal, is marked as a status region. Screen readers then announce the new value without the customer having to move there. The theme’s script itself is not changed. An area that also holds buttons or links, which would be read out with it, is left as it is.
- Rules it serves
-
-
vs-live-updatesAUTO
-
- File types it changes
-
.liquid
Before and after Example data sections/main-cart.liquid Changes to sections/main-cart.liquid: 1 added, 1 removed
Changes to sections/main-cart.liquid: 1 added, 1 removed Line before Line after Change Code 37 37 38 38 <div class="cart__footer"> 39 39 <h2 class="cart__summary-title">Order summary</h2> 40 Removed <p class="totals"> 40 Added <p class="totals" role="status"> 41 41 <span class="totals__label">{{ 'sections.cart.subtotal' | t }}</span> 42 42 <span class="totals__value" data-cart-subtotal>{{ cart.total_price | money_with_currency }}</span> 43 43 </p> sections/main-product.liquid Changes to sections/main-product.liquid: 1 added, 1 removed
Changes to sections/main-product.liquid: 1 added, 1 removed Line before Line after Change Code 71 71 </div> 72 72 {%- endif %} 73 73 74 Removed <div class="product__price" data-price-container> 74 Added <div class="product__price" data-price-container role="status"> 75 75 {% render 'price', product: product, use_variant: true %} 76 76 <p class="product__inventory" data-stock>{% if current_variant.available %}{{ 'products.product.in_stock' | t }}{% else %}{{ 'products.product.out_of_stock' | t }}{% endif %}</p> 77 77 <p class="product__sku">{{ 'products.product.sku' | t }}: <span data-sku>{{ current_variant.sku }}</span></p>
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 that says what they do, where the code shows their purpose.
Buttons and links that show only an icon, such as search, cart, account, slideshow arrows, quantity buttons and cookie choices, get a name that says what they do, where the code shows their purpose. The name is visually hidden text inside the control, or an
aria-labelwhere the theme has no class for hidden text. A logo link whose image has no text alternative gets your shop name. A control whose name would have to come from your own content is left to a person.- Rules it serves
-
-
button-nameASSISTED -
link-nameASSISTED -
image-altASSISTED -
vs-carouselAUTO -
vs-cookie-bannerASSISTED
-
- File types it changes
-
.json.liquid
buttons Example data locales/en.default.json Changes to locales/en.default.json: 3 added, 1 removed
Changes to locales/en.default.json: 3 added, 1 removed Line before Line after Change Code 34 34 "total_reviews": "{{ count }} reviews", 35 35 "link_messages": { 36 36 "new_window": "Opens in a new window." 37 Removed } 37 Added }, 38 Added "accept_cookies": "Accept cookies", 39 Added "reject_cookies": "Reject cookies" 38 40 }, 39 41 "sections": { 40 42 "header": { sections/header.liquid Changes to sections/header.liquid: 1 added, 0 removed
Changes to sections/header.liquid: 1 added, 0 removed Line before Line after Change Code 40 40 <input class="search__input field__input" type="search" name="q" value="" placeholder="{{ 'general.search.placeholder' | t }}" tabindex="1"> 41 41 <button type="submit" class="search__button"> 42 42 {%- render 'icon', icon: 'search', size: 20 -%} 43 Added <span class="visually-hidden">{{ 'general.search.search' | t }}</span> 43 44 </button> 44 45 </form> 45 46 </div> sections/main-product.liquid Changes to sections/main-product.liquid: 1 added, 1 removed
Changes to sections/main-product.liquid: 1 added, 1 removed Line before Line after Change Code 88 88 <p class="form__label">{{ option.name | escape }}</p> 89 89 <div class="swatch-list swatch-list--compact"> 90 90 {%- for swatch in option.swatches %} 91 Removed <button type="button" class="swatch swatch--button{% if swatch.selected %} swatch--active{% endif %}" style="--swatch-color: {{ swatch.color }};" data-option-index="{{ option_index }}" data-value="{{ swatch.value | escape }}"></button> 91 Added <button type="button" class="swatch swatch--button{% if swatch.selected %} swatch--active{% endif %}" style="--swatch-color: {{ swatch.color }};" data-option-index="{{ option_index }}" data-value="{{ swatch.value | escape }}"><span class="visually-hidden">{{ swatch.value | escape }}</span></button> 92 92 {%- endfor %} 93 93 </div> 94 94 </div> sections/slideshow.liquid Changes to sections/slideshow.liquid: 2 added, 0 removed
Changes to sections/slideshow.liquid: 2 added, 0 removed Line before Line after Change Code 59 59 <div class="slideshow__controls"> 60 60 <button type="button" class="slider-button slider-button--prev" name="previous" data-slide-prev> 61 61 {%- render 'icon', icon: 'chevron-left', size: 20 -%} 62 Added <span class="visually-hidden">{{ 'accessibility.previous_slide' | t }}</span> 62 63 </button> 63 64 <div class="slider-counter"> 64 65 {%- for block in section.blocks %} 67 68 </div> 68 69 <button type="button" class="slider-button slider-button--next" name="next" data-slide-next> 69 70 {%- render 'icon', icon: 'chevron-right', size: 20 -%} 71 Added <span class="visually-hidden">{{ 'accessibility.next_slide' | t }}</span> 70 72 </button> 71 73 </div> 72 74 </slideshow-component> snippets/cookie-banner.liquid Changes to snippets/cookie-banner.liquid: 2 added, 0 removed
Changes to snippets/cookie-banner.liquid: 2 added, 0 removed Line before Line after Change Code 23 23 <div class="cookie-banner__actions"> 24 24 <button type="button" class="cookie-banner__icon-button cookie-banner__icon-button--accept" data-cookie-accept> 25 25 {%- render 'icon', icon: 'check', size: 20 -%} 26 Added <span class="visually-hidden">{{ 'accessibility.accept_cookies' | t }}</span> 26 27 </button> 27 28 <button type="button" class="cookie-banner__icon-button" data-cookie-reject> 28 29 {%- render 'icon', icon: 'close', size: 20 -%} 30 Added <span class="visually-hidden">{{ 'accessibility.reject_cookies' | t }}</span> 29 31 </button> 30 32 </div> 31 33 </div> snippets/quantity-input.liquid Changes to snippets/quantity-input.liquid: 2 added, 0 removed
Changes to snippets/quantity-input.liquid: 2 added, 0 removed Line before Line after Change Code 9 9 {%- endcomment -%} 10 10 <div class="quantity quantity--compact" data-quantity{% if line %} data-line="{{ line }}"{% endif %}><button class="quantity__button" name="minus" type="button" data-quantity-minus> 11 11 {%- render 'icon', icon: 'minus', size: 12 -%} 12 Added <span class="visually-hidden">{{ 'products.product.quantity.decrease' | t: product: product_title }}</span> 12 13 </button> 13 14 <input class="quantity__input" type="number" name="{{ input_name }}" id="{{ input_id }}" value="{{ value | default: 1 }}" min="{% if line %}0{% else %}1{% endif %}" data-quantity-input> 14 15 <button class="quantity__button" name="plus" type="button" data-quantity-plus> 15 16 {%- render 'icon', icon: 'plus', size: 12 -%} 17 Added <span class="visually-hidden">{{ 'products.product.quantity.increase' | t: product: product_title }}</span> 16 18 </button></div> links Example data sections/footer.liquid Changes to sections/footer.liquid: 1 added, 0 removed
Changes to sections/footer.liquid: 1 added, 0 removed Line before Line after Change Code 37 37 {%- for social in shop.social %} 38 38 <a href="{{ social.url }}" class="list-social__link link"> 39 39 {%- render 'icon', icon: social.icon, size: 20 -%} 40 Added <span class="visually-hidden">{{ 'general.social.links.' | append: social.icon | t }}</span> 40 41 </a> 41 42 {%- endfor %} 42 43 </ul> sections/header.liquid Changes to sections/header.liquid: 3 added, 1 removed
Changes to sections/header.liquid: 3 added, 1 removed Line before Line after Change Code 22 22 </details> 23 23 24 24 {% if logo_is_heading %}<h1 class="header__heading">{% else %}<div class="header__heading">{% endif %} 25 Removed <a href="{{ routes.root_url }}" class="header__heading-link"><img src="{{ 'logo.svg' | asset_url }}" width="{{ settings.logo_width }}" height="32" class="header__heading-logo"></a> 25 Added <a href="{{ routes.root_url }}" class="header__heading-link"><img src="{{ 'logo.svg' | asset_url }}" width="{{ settings.logo_width }}" height="32" class="header__heading-logo" alt="{{ shop.name | escape }}"></a> 26 26 {% if logo_is_heading %}</h1>{% else %}</div>{% endif %} 27 27 28 28 <nav class="header__inline-menu"> 50 50 {%- endif %} 51 51 <a href="{{ routes.account_login_url }}" class="header__icon header__icon--account link"> 52 52 {%- render 'icon', icon: 'account', size: icon_size -%} 53 Added <span class="visually-hidden">{{ 'sections.header.account' | t }}</span> 53 54 </a> 54 55 <a href="{{ routes.cart_url }}" class="header__icon header__icon--cart link" id="cart-icon-bubble"> 55 56 {%- render 'icon', icon: 'cart', size: icon_size -%} 57 58 <span class="cart-count-bubble" aria-hidden="true"><span data-cart-count>{{ cart.item_count }}</span></span> 58 59 {%- endif -%} 59 60 61 Added <span class="visually-hidden">{{ 'sections.header.cart' | t }}</span> 60 62 </a> 61 63 </div> 62 64 </header> sections/main-cart.liquid Changes to sections/main-cart.liquid: 1 added, 0 removed
Changes to sections/main-cart.liquid: 1 added, 0 removed Line before Line after Change Code 26 26 {% render 'quantity-input', input_id: qty_id, input_name: 'updates[]', value: item.quantity, product_title: item.product.title, line: line %} 27 27 <a href="{{ routes.cart_change_url }}?line={{ line }}&quantity=0" class="cart-item__remove cart-item__remove--compact" data-cart-remove> 28 28 {%- render 'icon', icon: 'trash', size: 16 -%} 29 Added <span class="visually-hidden">{{ 'sections.cart.remove' | t: title: item.product.title }}</span> 29 30 </a> 30 31 </div> 31 32 <div class="cart-item__totals"> -
Decorative images hidden from screen readers decorative-image-alt Only images the code proves decorative are hidden from screen readers; 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, it is an 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 reading out a file name or the word “image”. 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
-
.liquid
Before and after Example data sections/footer.liquid Changes to sections/footer.liquid: 1 added, 1 removed
Changes to sections/footer.liquid: 1 added, 1 removed Line before Line after Change Code 12 12 <input type="hidden" name="contact[tags]" value="newsletter"> 13 13 <div class="newsletter-form__field-wrapper"> 14 14 <input type="email" name="contact[email]" class="field__input" placeholder="{{ 'newsletter.label' | t }}" autocorrect="off" autocapitalize="off"> 15 Removed <button type="submit" class="button newsletter-form__button" name="commit"><img src="{{ 'logo.svg' | asset_url }}" class="newsletter-form__icon" width="16" height="16">{{ 'newsletter.button' | t }}</button> 15 Added <button type="submit" class="button newsletter-form__button" name="commit"><img src="{{ 'logo.svg' | asset_url }}" class="newsletter-form__icon" width="16" height="16" alt="">{{ 'newsletter.button' | t }}</button> 16 16 </div> 17 17 {%- endform -%} 18 18 </div> sections/header.liquid Changes to sections/header.liquid: 1 added, 1 removed
Changes to sections/header.liquid: 1 added, 1 removed Line before Line after Change Code 46 46 47 47 <div class="header__icons"> 48 48 {%- if section.settings.show_help_link %} 49 Removed <a href="{{ section.settings.help_link_url }}" class="header__utility-link link"><svg class="header__utility-icon" role="img" viewBox="0 0 24 24" width="16" height="16"><circle cx="12" cy="12" r="9"/></svg>{{ section.settings.help_link_label | escape }}</a> 49 Added <a href="{{ section.settings.help_link_url }}" class="header__utility-link link"><svg class="header__utility-icon" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/></svg>{{ section.settings.help_link_label | escape }}</a> 50 50 {%- endif %} 51 51 <a href="{{ routes.account_login_url }}" class="header__icon header__icon--account link"> 52 52 {%- render 'icon', icon: 'account', size: icon_size -%} -
Titles for embedded maps and videos iframe-title An embedded frame without a title gets a translated title that says what it shows, where its source address or content tells.
An embedded frame without a title, such as a map or a YouTube or Vimeo video, gets a translated title that says what it shows. The recipe reads that from the frame’s source address or content. 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
-
.json.liquid
Before and after Example data locales/en.default.json Changes to locales/en.default.json: 2 added, 1 removed
Changes to locales/en.default.json: 2 added, 1 removed Line before Line after Change Code 34 34 "total_reviews": "{{ count }} reviews", 35 35 "link_messages": { 36 36 "new_window": "Opens in a new window." 37 Removed } 37 Added }, 38 Added "embed_map": "Map" 38 39 }, 39 40 "sections": { 40 41 "header": { sections/contact-form.liquid Changes to sections/contact-form.liquid: 1 added, 1 removed
Changes to sections/contact-form.liquid: 1 added, 1 removed Line before Line after Change Code 8 8 {%- endcapture -%} 9 9 <div class="contact-section page-width page-width--narrow"> 10 10 <h2 class="contact-section__heading">{{ section.settings.map_heading | escape }}</h2> 11 Removed <iframe class="store-map" srcdoc="{{ map_document | escape }}" width="600" height="360"></iframe> 11 Added <iframe class="store-map" srcdoc="{{ map_document | escape }}" width="600" height="360" title="{{ 'accessibility.embed_map' | t | escape }}"></iframe> 12 12 13 13 <h2 class="contact-section__heading">{{ 'templates.contact.form.title' | t }}</h2> 14 14 {%- form 'contact', id: 'ContactForm', class: 'contact__form' %}
Details: the tiers, the test theme, and text in your store’s languages
A recipe is a deterministic change for one kind of failure: the same code always gets the same change. Validiscan has 20 recipes for Shopify themes. Each one makes a change only where the theme’s code shows what the right change is; where it does not, the recipe changes nothing 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. They run on its Shopify test theme, a Liquid theme built like Shopify’s Dawn theme with accessibility failures put in on purpose, and check that the failure is gone after the change; most of them also check that the change causes no new failure of the same rule. Your theme’s files will look different. The test theme still contains a layout/checkout.liquid file and a checkout field snippet, snippets/, so some of the diffs change them; as described above, Shopify no longer uses that file for the information, shipping and payment steps.
When a recipe adds text, such as the words of a skip link or the name of an icon button, it reuses a translation the theme already has where it can. Otherwise it adds the text in English, and in German where the theme has a German locale file. Text in the other languages of your store is not added: a theme whose default language is neither English nor German gets the English text there.
What stays MANUAL on Shopify?
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 Shopify. 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 a scan and have the tier MANUAL on Shopify.
-
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, or another way to use a control that only works by dragging. The focus order check vs-focus-order has the tier MANUAL because a focus order that does not match the screen is a question of how the templates are ordered; where one of its findings is about a tabindex above 0, the tab order recipe above removes that tabindex.
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 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 five things for a Shopify project: written permission for a full scan of your store, one way into your theme, your answer on whether AI may draft fixes of tier ASSISTED, a review of the changes, and your decisions on what stays MANUAL.
- Written permission for a full scan of your store. 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 it, a Theme Access password and your store’s
myshopify.comaddress, or a theme file uploaded in the customer portal. - Your answer on whether AI may draft fixes of tier ASSISTED. AI drafting is off until you agree.
- A review of the changes: publish the theme copy, merge the pull request or apply the patch bundle.
- 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
6 sources, each checked for this page on 6 October 2026.
Shopify’s documentation, Help Center and App Store 6
- Manage theme access · Shopify · checked
- Downloading themes · Shopify Help Center · checked
- Publishing themes · Shopify Help Center · checked
- Shopify GitHub integration for themes · Shopify · checked
- checkout.liquid · Shopify · checked
- Theme Access · Shopify App Store · 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