Sample record on the public demo pages of Shopify's Dawn theme. No person has tested these pages by hand. All 8 NVDA rows were run by a script on saved copies of the pages, and each shows what NVDA said.
Sample record on Shopify's Dawn theme
How this record was made
Made on 5 November 2026 by an AI assistant (Claude) working for Novruz Veliiev, using scripts. No person has tested these pages by hand. The issues and the patches are real. They come from the public demo store and from the Dawn source code, and the steps can be repeated.
- Pages: three public pages of the demo store theme-dawn-demo.myshopify.com: /, /collections/bags, /products/small-naomi-gummy. The store reports the theme as Dawn 15.0.0.
- Tools: Playwright 1.64.1 driving Chromium 156.0.8078.4, axe-core 4.14.0, the browser's accessibility tree, and pixel comparison of screenshots.
- Keyboard script: a script pressed Tab, Shift+Tab, Enter on disclosure buttons, and Escape, and recorded each stop. It is a script, not a person at a keyboard.
- Limits kept: no purchase, no form sent, no cart change and no store data changed. Requests the store's robots.txt disallows were blocked (10 requests).
- Patches: written for the Dawn 16.1.1 source, commit 3e9ee22. Each applies cleanly to that source. The same code is in the 15.0.0 version the demo runs.
- After state: not on a Shopify store. The saved demo pages were replayed in a local browser with no network, with the same changes made to the saved files. So these numbers show the change in a browser, not a live theme. See how the after state was checked.
- NVDA: All 8 NVDA rows were run by a script on saved copies of the pages, and each shows what NVDA said. Real NVDA 2026.3 on Windows 11 with Chrome 156. The voice was silent and the spoken lines come from NVDA's own log, so no person listened. A row changes only when a real NVDA speech log is attached.
A paid job is different. On a paid order a person tests your store by hand with a keyboard and screen readers, on your theme, and the record gives that person's results and dates.
I am a software tester, not a law firm. This is a technical record and not legal advice. It makes no promise about any case and makes no statement that any store meets any law or standard. It is not a statement about Shopify. Shopify did not make or endorse it.
What is on this page
- Numbers from the scripts
- Six issues with patches
- Before and after
- Seen, not patched here
- Looked at and not counted
- NVDA rows
- How the after state was checked
- What was not checked
- Patch files and license
Numbers from the scripts
| Check | Done by |
|---|---|
| WCAG A and AA rules at load, after scrolling, at 320 px and with menus open | Done by: Tool: axe-core |
| Tab order, Tab stops, traps, menus and dialogs opening and closing | Done by: Keyboard script in Playwright |
| Name, role and state at each stop | Done by: Script reading the browser's accessibility tree |
| Focus ring, focus out of view, contrast over a photo | Done by: Script comparing screenshots and reading positions |
| Sideways scroll at 640 px and 320 px | Done by: Script |
| What a screen reader says | Done by: Real NVDA, with a script sending the keys. No person listened. |
| Measure | Home | Collection | Product |
|---|---|---|---|
| Page | Home: / | Collection: /collections/bags | Product: /products/small-naomi-gummy |
| axe-core, WCAG A and AA rules, public page at load: rules failed (text nodes) | Home: 1 (13) | Collection: 1 (25) | Product: 1 (9) |
| Same rules after scrolling through the page, local replay | Home: 0 (0) | Collection: 0 (0) | Product: 0 (0) |
| axe best-practice rules failed, 1280 px | Home: 0 | Collection: 0 | Product: 0 |
| axe WCAG rules failed at 320 px | Home: 0 | Collection: 0 | Product: 0 |
| axe with the footer country selector open | Home: aria-required-attr, aria-valid-attr-value | Collection: aria-required-attr, aria-valid-attr-value | Product: aria-required-attr, aria-valid-attr-value |
| Keyboard stops by script, 1280 px and 320 px | Home: 38 and 35 | Collection: 47 and 41 | Product: 38 and 49 |
| Stops with no accessible name, 1280 px | Home: 0 | Collection: 0 | Product: 0 |
| Keyboard traps found by script | Home: 0 | Collection: 0 | Product: 0 |
| Sideways scroll at 640 px and 320 px, in px | Home: 0 and 0 | Collection: 0 and 0 | Product: 0 and 0 |
| Images in the page and images with empty alt | Home: 25 and 9 | Collection: 33 and 16 | Product: 30 and 12 |
Six issues with patches
Each issue says how it was seen. "Scripted keyboard walk" means the Playwright script. None of these was found or confirmed by a person. The NVDA line under an issue says what NVDA spoke at that place when a script pressed the keys.
D-01: Enlarged product image has no keyboard route on a desktop
- Place
- Product page, gallery. Window 1280 by 900 for the missing route, width 320 for the focused image out of view.
- What happens
- A mouse user can click an image to magnify it. At 1280 px the script went from Skip to product information straight to the first color option, with 0 stops in the gallery. The button Open media 1 in modal is in the page for each of the 12 images, but has display: none on devices with a mouse. At 320 px the buttons can be reached, but the browser leaves a partly visible image where it is: 15 of 286 px of image 2 shows while its button has focus.
- WCAG 2.1 criterion
- 2.1.1 Keyboard
- How it was seen
- Scripted keyboard walk, 38 stops at 1280 px. The CSS rule read in the Dawn source.
- Fix
- Keep the button in the tab order, let pointer events pass through it so hover zoom still works, size it like the image, and scroll a focused slide into view. Patch: 01-zoom-keyboard-route.patch.
- After the fix
- 50 stops at 1280 px, 12 of them in the gallery. Enter opens the image dialog, focus moves into it, Tab stays inside it, Escape closes it and focus returns to the button. At 320 px, 286 of 286 px of image 2 shows while its button has focus. A scripted mouse click on the image still starts hover zoom and opens no dialog.
- How the after state was checked
- Local browser replay, keyboard script and axe with the dialog open (0 rules failed). Not checked on a real store.
- NVDA
- N-01 (after): Yes for the button and for Escape. Tab lands on the button and NVDA says Open media 1 in modal, button, opens dialog. Enter: NVDA says the name Media gallery and reads on from the Close button, but does not say the word dialog. Tab: Close, button. Escape returns to the button.
Show the code change for D-01
diff --git a/assets/global.js b/assets/global.js
index bde1d32..d4e6e35 100644
--- a/assets/global.js
+++ b/assets/global.js
@@ -749,6 +749,12 @@ class SliderComponent extends HTMLElement {
if (!this.slider || !this.nextButton) return;
+ // The browser leaves a partly visible slide where it is when something inside it takes focus.
+ this.slider.addEventListener('focusin', (event) => {
+ const slide = event.target.closest('[id^="Slide-"]');
+ if (slide) slide.scrollIntoView({ block: 'nearest', inline: 'nearest' });
+ });
+
this.initPages();
const resizeObserver = new ResizeObserver((entries) => this.initPages());
resizeObserver.observe(this.slider);
diff --git a/assets/section-main-product.css b/assets/section-main-product.css
index c9dfc58..db10754 100644
--- a/assets/section-main-product.css
+++ b/assets/section-main-product.css
@@ -553,8 +553,11 @@ a.product__text {
position: relative;
}
+/* Sized like the image, so that focus scrolls the whole image into view. */
.product__media-toggle {
display: flex;
+ position: absolute;
+ inset: 0;
border: none;
background-color: transparent;
color: currentColor;
@@ -1069,12 +1072,19 @@ a.product__text {
}
@media (hover: hover) {
- .product__media-zoom-hover,
.product__media-icon--hover {
display: none;
}
}
+/* Keep the button in the tab order so keyboard users can open the image dialog.
+ Pointer events pass through it, so hover zoom works as before. */
+@media (hover: hover) and (min-width: 750px) {
+ .product__media-zoom-hover:not(:focus-visible) {
+ pointer-events: none;
+ }
+}
+
@media screen and (max-width: 749px) {
.product__media-zoom-hover,
.product__media-icon--hover {
D-02: Keyboard focus can end up out of sight
- Place
- Product page at 1280 by 900: accordion buttons Dimensions and Care Instructions. Home page at 1280 by 900: product link Mini Eddy.
- What happens
- On the product page the information column is sticky. When it is taller than the window, scrolling cannot bring its lower controls into view: Dimensions sat at y=929 and Care Instructions at y=980 in a 900 px window while they had focus. On the home page, moving up to Mini Eddy put it at y=0 under the sticky header, and 0 pixels changed around it when it got focus.
- WCAG 2.1 criterion
- 2.4.7 Focus Visible
- How it was seen
- Scripted keyboard walk. Position from getBoundingClientRect, the top element from elementFromPoint, and a pixel comparison of focused and unfocused screenshots.
- Fix
- Turn off the sticky position while keyboard focus is inside the column. Keep room for the header height when focus scrolls the page. Patch: 02-focus-stays-in-view.patch.
- After the fix
- Dimensions at y=570, Care Instructions at y=621, both in the window. Mini Eddy at y=484, not covered, with 574 pixels changing on focus at 4.74:1.
- How the after state was checked
- Local browser replay, same keyboard script. Not checked on a real store.
- NVDA
- No speech row. This is a visual issue.
Show the code change for D-02
diff --git a/assets/base.css b/assets/base.css
index 6ad490f..755e91f 100644
--- a/assets/base.css
+++ b/assets/base.css
@@ -231,6 +231,11 @@ body:has(.section-header .drawer-menu) .announcement-bar-section .page-width {
overflow: hidden;
}
+/* Keep room for the sticky header when focus or an anchor scrolls the page. */
+html {
+ scroll-padding-top: var(--header-height, 0px);
+}
+
.skip-to-content-link:focus {
z-index: 9999;
position: inherit;
diff --git a/assets/section-main-product.css b/assets/section-main-product.css
index c9dfc58..cc5be32 100644
--- a/assets/section-main-product.css
+++ b/assets/section-main-product.css
@@ -27,6 +27,11 @@ product-info {
z-index: 2;
}
+ /* A sticky column taller than the window can hold the focused control below the fold. */
+ .product__column-sticky:has(:focus-visible) {
+ position: static;
+ }
+
.product--thumbnail .thumbnail-list {
padding-right: var(--media-shadow-horizontal-offset);
}
D-03: Decrease quantity looks inactive but is not exposed as inactive, and its focus ring is faint
- Place
- Product page, quantity control, at quantity 1.
- What happens
- The button has the class disabled and looks dimmed. The browser's accessibility tree reports an ordinary enabled button, disabled=false. From the source, pressing it at the minimum changes nothing; the script does not press quantity buttons. Opacity 0.5 on the button also dims its focus ring, measured at 2.21:1.
- WCAG 2.1 criterion
- 4.1.2 Name, Role, Value and 1.4.11 Non-text Contrast
- How it was seen
- Accessibility tree through the Chrome DevTools Protocol. Pixel comparison of focused and unfocused screenshots, highest contrast of any changed pixel.
- Fix
- Set aria-disabled together with the class, for both quantity buttons. Dim the icon only, not the whole button. Patch: 03-quantity-button-state.patch.
- After the fix
- The accessibility tree reports disabled=true. Focus ring measured at 4.74:1.
- How the after state was checked
- Local browser replay, same script. Row N-03 has what NVDA said.
- NVDA
- N-02 (before): Nothing shows it is unavailable. NVDA says the name and button.
N-03 (after): Yes. NVDA adds unavailable.
Show the code change for D-03
diff --git a/assets/base.css b/assets/base.css
index 6ad490f..e8c5834 100644
--- a/assets/base.css
+++ b/assets/base.css
@@ -1352,9 +1352,17 @@ shopify-accelerated-checkout-cart {
.button.disabled,
.customer button:disabled,
.customer button[aria-disabled='true'],
-.customer button.disabled,
+.customer button.disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+/* Dim the icon only. Opacity on the button would also dim its focus ring. */
.quantity__button.disabled {
cursor: not-allowed;
+}
+
+.quantity__button.disabled svg {
opacity: 0.5;
}
diff --git a/assets/global.js b/assets/global.js
index bde1d32..a5535b6 100644
--- a/assets/global.js
+++ b/assets/global.js
@@ -267,12 +267,16 @@ class QuantityInput extends HTMLElement {
const value = parseInt(this.input.value);
if (this.input.min) {
const buttonMinus = this.querySelector(".quantity__button[name='minus']");
- buttonMinus.classList.toggle('disabled', parseInt(value) <= parseInt(this.input.min));
+ const atMin = parseInt(value) <= parseInt(this.input.min);
+ buttonMinus.classList.toggle('disabled', atMin);
+ buttonMinus.setAttribute('aria-disabled', atMin);
}
if (this.input.max) {
const max = parseInt(this.input.max);
const buttonPlus = this.querySelector(".quantity__button[name='plus']");
- buttonPlus.classList.toggle('disabled', value >= max);
+ const atMax = value >= max;
+ buttonPlus.classList.toggle('disabled', atMax);
+ buttonPlus.setAttribute('aria-disabled', atMax);
}
}
}
D-04: Country search field has invalid ARIA and a repeated id
- Place
- Footer on all three pages, once the country selector is open. The same snippet is used in the header drawer.
- What happens
- The field has role=combobox with no aria-expanded, and aria-owns and aria-controls point at an id that does not exist on the page. The id country-filter-input is repeated in each copy on the page, so the label in the footer points at the copy in the drawer.
- WCAG 2.1 criterion
- 4.1.2 Name, Role, Value and 1.3.1 Info and Relationships
- How it was seen
- axe-core with the selector open: aria-required-attr, aria-valid-attr-value, both critical, on all three pages. With the selector closed, axe only lists duplicate-id-aria as needing review.
- Fix
- Make it a plain search field with one id per position, a label tied to that id, and aria-controls pointing at the real list. Patch: 04-country-search-field.patch.
- After the fix
- axe with the selector open: none. duplicate-id-aria is no longer listed. The accessibility tree reports role searchbox, name Search; before it was role combobox.
- How the after state was checked
- Local browser replay with the rendered HTML changed to match the patch. LiquidJS render of the patched snippet with stub filters. Not checked on a real store.
- NVDA
- N-04 (before): NVDA says Search, combo box, has auto complete, editable, blank.
N-05 (after): NVDA says Search, edit, blank. The field is read as a plain edit field with its name.
Show the code change for D-04
diff --git a/snippets/country-localization.liquid b/snippets/country-localization.liquid
index 9efa9a6..7f75b51 100644
--- a/snippets/country-localization.liquid
+++ b/snippets/country-localization.liquid
@@ -48,22 +48,20 @@
<div class="field">
<input
class="country-filter__input field__input"
- id="country-filter-input"
+ id="{{ localPosition }}-country-filter-input"
type="search"
name="country_filter"
value=""
placeholder="{{ 'localization.search' | t }}"
- role="combobox"
- aria-owns="country-results"
- aria-controls="country-results"
- aria-haspopup="listbox"
- aria-autocomplete="list"
+ aria-controls="{{ localPosition }}-country-results"
autocorrect="off"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
>
- <label class="field__label" for="country-filter-input">{{ 'localization.search' | t }}</label>
+ <label class="field__label" for="{{ localPosition }}-country-filter-input">
+ {{- 'localization.search' | t -}}
+ </label>
<button
type="reset"
class="country-filter__reset-button field__button hidden"
D-05: Product gallery images have empty alt text
- Place
- Product page, gallery, all 12 images.
- What happens
- All 12 gallery images have alt="" in the page, and the accessibility tree shows 12 empty list items. NVDA reads the list as one item with one Unlabeled graphic and leaves the other 11 images unspoken. The same images have the alt text Small Naomi in the image dialog, so the store has text for them. The gallery does not use it.
- WCAG 2.1 criterion
- 1.1.1 Non-text Content
- How it was seen
- The saved page and the accessibility tree snapshot. axe reports nothing here, because alt="" is valid markup.
- Fix
- Pass media.alt to the image tag in product-thumbnail.liquid, as the image dialog already does. Patch: 05-gallery-alt-text.patch.
- After the fix
- 0 gallery images have empty alt. The accessibility tree shows 12 images named Small Naomi on the page, up from 0.
- How the after state was checked
- Local browser replay with the rendered HTML changed to match the patch, using the text the store's own image dialog shows. LiquidJS render of the patched snippet with stub filters. Not checked on a real store. Twelve images with one text is not a good description: better text per image is store data, and it was not changed here.
- NVDA
- N-07 (before): One image out of 12. NVDA says list, with 1 item, Unlabeled graphic, clickable. The other 11 images are not spoken.
N-08 (after): Yes. NVDA says list, with 12 items, and reads all 12 images as graphic, Small Naomi, each next to its button Open media 12 to 1 in modal.
Show the code change for D-05
diff --git a/snippets/product-thumbnail.liquid b/snippets/product-thumbnail.liquid
index 4eefa39..42b5086 100644
--- a/snippets/product-thumbnail.liquid
+++ b/snippets/product-thumbnail.liquid
@@ -78,6 +78,7 @@
| image_url: width: 1946
| image_tag:
class: image_class,
+ alt: media.alt,
loading: lazy,
sizes: sizes,
widths: '246, 493, 600, 713, 823, 990, 1100, 1206, 1346, 1426, 1646, 1946'
D-06: Banner subtitle over the photo measures under 4.5:1
- Place
- Home page, image banner, the line Atypical leather goods, 1280 by 900.
- What happens
- Text at 75% alpha over a darkened photo. Measured from pixels: #d3d8dc on the most common background color #566978, 3.97:1. axe does not decide this one. It lists the text as needing review.
- WCAG 2.1 criterion
- 1.4.3 Contrast (Minimum)
- How it was seen
- Pixel measurement of a screenshot of the text box: the most common color against the color furthest from it. The background of a photo varies, so this is one reading and not a full map.
- Fix
- Use the full foreground color for text in the banner box. Patch: 06-banner-text-contrast.patch.
- After the fix
- #fefefe on the same background, 5.65:1, same measurement.
- How the after state was checked
- Local browser replay, same measurement. A store with a lighter photo can still fall short.
- NVDA
- No speech row. This is a visual issue.
Show the code change for D-06
diff --git a/assets/section-image-banner.css b/assets/section-image-banner.css
index e446abe..3940907 100644
--- a/assets/section-image-banner.css
+++ b/assets/section-image-banner.css
@@ -399,6 +399,11 @@
margin-top: 1.5rem;
}
+/* Text over a photo gets the full foreground color, not the 75% default. */
+.banner__box .banner__text {
+ color: rgb(var(--color-foreground));
+}
+
@media screen and (min-width: 750px) {
.banner__box > * + .banner__text {
margin-top: 2rem;
Before and after
| Issue and measure | Before | After |
|---|---|---|
| D-01: Tab stops in the product gallery, 1280 px | Before: 0 | After: 12 |
| D-01: Width of image 2 in view when its button has focus, 320 px | Before: 15 of 286 px | After: 286 of 286 px |
| D-01: First 5 gallery images fully in view when focused, 320 px | Before: 1 of 5 | After: 5 of 5 |
| D-02: Product page stops outside the window | Before: 2 | After: 0 |
| D-02: Home page stops covered by the header | Before: 1 | After: 0 |
| D-03: Accessibility tree, Decrease button disabled | Before: false | After: true |
| D-03: Focus ring contrast on that button | Before: 2.21:1 | After: 4.74:1 |
| D-04: axe rules failed with the country selector open | Before: aria-required-attr, aria-valid-attr-value | After: none |
| D-05: Gallery images with empty alt text | Before: 12 | After: 0 |
| D-06: Banner subtitle contrast from pixels | Before: 3.97:1 | After: 5.65:1 |
Seen, not patched here
- O-01. Collection page at width 320, Filter and sort drawer: when the drawer is open, the first Tab lands on the close icon, a span with tabindex=0. The accessibility tree gives it role generic and an empty name. NVDA says only button, expanded, with no name (row N-06). Enter on it closes the drawer in Chromium. WCAG 4.1.2 Name, Role, Value. Not patched in this sample, which is limited to six issues. The fix would be to remove the tabindex from that span in facets.liquid.
Looked at and not counted
- At page load axe reported color-contrast on 47 text nodes across the three pages, all #fdfdfd on white at 1.01:1. These sit below the fold and are still faded out by Dawn's scroll animation. After scrolling through each page the same rule reports 0, so they are not counted.
- Empty alt text on product cards. On the collection page the 16 images with empty alt are the second images of the cards, shown on hover, and the card's link carries the product name. On the home page 8 of the 9 are the same. So they are not counted. The ninth, a multicolumn image on the home page, was not judged.
- Escape does not close a product accordion. Accordions are not menus, so this is not counted.
NVDA rows
A row says what real NVDA spoke, read from NVDA's own log. Both the "before" and the "after" rows ran on the local replay of the saved demo pages, not on the live store. A script sent the keys and no person listened.
| Row | Page and state | Keys | What the row asks | Result |
|---|---|---|---|---|
| N-01 (D-01) | Page and state: Product page, after | Keys: Tab from Skip to product information. Then Enter. Then Tab. Then Escape. | What the row asks: Is the next stop announced as Open media 1 in modal, button? Does Enter announce a dialog? Does Escape return to the button? | Result: Yes for the button and for Escape. Tab lands on the button and NVDA says Open media 1 in modal, button, opens dialog. Enter: NVDA says the name Media gallery and reads on from the Close button, but does not say the word dialog. Tab: Close, button. Escape returns to the button. Speech log: Open media 1 in modal, button, opens dialog / Media gallery / button, Close / graphic, Small Naomi / Close, button / Open media 1 in modal, button, opens dialog Run by an AI assistant (Claude), with a script sending the keys. No person listened, 5 November 2026, NVDA 2026.3, Chrome 156 on Windows 11 |
| N-02 (D-03) | Page and state: Product page, before | Keys: Tab to Decrease quantity at quantity 1. | What the row asks: Does NVDA say anything to show the button is unavailable? | Result: Nothing shows it is unavailable. NVDA says the name and button. Speech log: Decrease quantity for Small Naomi, button Run by an AI assistant (Claude), with a script sending the keys. No person listened, 5 November 2026, NVDA 2026.3, Chrome 156 on Windows 11 |
| N-03 (D-03) | Page and state: Product page, after | Keys: Tab to Decrease quantity at quantity 1. | What the row asks: Does NVDA say unavailable? | Result: Yes. NVDA adds unavailable. Speech log: Decrease quantity for Small Naomi, button, unavailable Run by an AI assistant (Claude), with a script sending the keys. No person listened, 5 November 2026, NVDA 2026.3, Chrome 156 on Windows 11 |
| N-04 (D-04) | Page and state: Footer on the home page, before | Keys: Tab to the country button in the footer. Press Enter. | What the row asks: What name and role does NVDA say for the search field that takes focus? | Result: NVDA says Search, combo box, has auto complete, editable, blank. Speech log: Search, combo box, has auto complete, editable, blank Run by an AI assistant (Claude), with a script sending the keys. No person listened, 5 November 2026, NVDA 2026.3, Chrome 156 on Windows 11 |
| N-05 (D-04) | Page and state: Footer on the home page, after | Keys: Tab to the country button in the footer. Press Enter. | What the row asks: What name and role does NVDA say for the search field that takes focus? | Result: NVDA says Search, edit, blank. The field is read as a plain edit field with its name. Speech log: Search, edit, Search, blank Run by an AI assistant (Claude), with a script sending the keys. No person listened, 5 November 2026, NVDA 2026.3, Chrome 156 on Windows 11 |
| N-06 (O-01) | Page and state: Collection page at 320 px, before | Keys: Tab to Filter and sort. Press Enter. Press Tab. | What the row asks: What does NVDA say at the stop after the button? | Result: NVDA says button, expanded, and gives no name. Speech log: button, expanded Run by an AI assistant (Claude), with a script sending the keys. No person listened, 5 November 2026, NVDA 2026.3, Chrome 156 on Windows 11 |
| N-07 (D-05) | Page and state: Product page, before | Keys: In browse mode press Up Arrow from below the heading Small Naomi, through the heading and the gallery above it. The key 1 was tried first and NVDA said No next heading at level 1, because its reading position was already below the heading. | What the row asks: Is anything spoken for the 12 gallery images? | Result: One image out of 12. NVDA says list, with 1 item, Unlabeled graphic, clickable. The other 11 images are not spoken. Speech log: heading, level 1, Small Naomi / Gallery Viewer, region, list, with 1 item, Unlabeled graphic, clickable, To get missing image descriptions, open the context menu. / out of list, same page, link, Skip to product information Run by an AI assistant (Claude), with a script sending the keys. No person listened, 5 November 2026, NVDA 2026.3, Chrome 156 on Windows 11 |
| N-08 (D-05) | Page and state: Product page, after | Keys: In browse mode press Up Arrow from below the heading Small Naomi, through the heading and the gallery above it. The key 1 was tried first and NVDA said No next heading at level 1, because its reading position was already below the heading. | What the row asks: Is each gallery image spoken as a graphic with the text Small Naomi? | Result: Yes. NVDA says list, with 12 items, and reads all 12 images as graphic, Small Naomi, each next to its button Open media 12 to 1 in modal. Speech log: heading, level 1, Small Naomi / Gallery Viewer, region, list, with 12 items, button, opens dialog, Open media 12 in modal / graphic, clickable, Small Naomi / button, opens dialog, Open media 11 in modal / graphic, Small Naomi / and so on to Open media 1 in modal / graphic, clickable, Small Naomi / out of list, same page, link, Skip to product information Run by an AI assistant (Claude), with a script sending the keys. No person listened, 5 November 2026, NVDA 2026.3, Chrome 156 on Windows 11 |
How the after state was checked
- Patches apply:
git apply --checkon Dawn 16.1.1, each alone and all six in order. All applied. - Theme linter:
shopify theme check, offline, on the source before and after the patches: 9 offenses before, 9 after, the same ones. The patches add none. - Liquid render: the two changed Liquid files rendered with LiquidJS and stub filters. This is not Shopify's renderer. The patched source passes 8 of 8 checks. The unpatched source fails 5 of them.
- Browser replay: the saved demo pages, replayed with no network, with the same changes made to the minified CSS and JavaScript and the rendered HTML. The same script ran on the replay before and after.
- Not done: no patch has run on a real Shopify store, and no person has checked the after state.
What was not checked
- NVDA on a live store. The NVDA rows ran on saved local copies of the demo pages. JAWS, VoiceOver and TalkBack: not run.
- Real phones and tablets. The 320 px checks are a narrow desktop window in Chromium.
- Firefox, Safari and Edge. Only Chromium was used.
- Cart, checkout, search results, filtering, sorting, variant change, quantity change, add to cart and form errors. The script made no shopping action and sent no form.
- The related products block on the product page. The store's robots.txt disallows that request, so it was blocked.
- The home page video. Only its poster button was reached; the video was not played, so captions were not checked.
- Banner heading contrast over the photo. The pixel method is not reliable for that text.
- Other page types, other Dawn settings and color schemes, and any app a merchant adds.
- A person's judgment on reading order, link text and the meaning of alt text.
Patch files and license
- 01-zoom-keyboard-route.patch
- 02-focus-stays-in-view.patch
- 03-quantity-button-state.patch
- 04-country-search-field.patch
- 05-gallery-alt-text.patch
- 06-banner-text-contrast.patch
Dawn is by Shopify. Its license is MIT-style with one added limit: the rights are for developing themes that work with Shopify. The patches contain short pieces of Dawn's code, so the license goes with them: LICENSE-dawn.txt. To try them, run git apply in a copy of the Dawn source with Unix line endings.
End of the sample record. No person has tested these pages by hand. All 8 NVDA rows were run by a script on saved copies of the pages, and each shows what NVDA said.