← Shopify App BuilderCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Shopify App Builder
Snapshot Sep 30, 2026 · 23:13 UTC · version 1.4.1
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Use when auditing or building accessibility in a Shopify embedded app — WCAG 2.1 AA, keyboard navigation, focus management in Modal/SaveBar/ResourcePicker, screen reader support (NVDA/JAWS/VoiceOver), color contrast within Polaris tokens, ARIA usage, alt text, i18n + a11y, and Built for Shopify accessibility gates. Triggers: 'accessibility shopify app', 'a11y shopify', 'WCAG 2.1 AA', 'screen reader shopify', 'keyboard nav shopify app', 'focus management modal', 'polaris contrast', 'color contrast shopify', 'aria label polaris', 'shopify accessibility audit', 'built for shopify accessibility'.",
"included_files": [],
"name": "app-accessibility",
"skill_md_contents": "---\nname: app-accessibility\ndescription: \"Use when auditing or building accessibility in a Shopify embedded app — WCAG 2.1 AA, keyboard navigation, focus management in Modal/SaveBar/ResourcePicker, screen reader support (NVDA/JAWS/VoiceOver), color contrast within Polaris tokens, ARIA usage, alt text, i18n + a11y, and Built for Shopify accessibility gates. Triggers: 'accessibility shopify app', 'a11y shopify', 'WCAG 2.1 AA', 'screen reader shopify', 'keyboard nav shopify app', 'focus management modal', 'polaris contrast', 'color contrast shopify', 'aria label polaris', 'shopify accessibility audit', 'built for shopify accessibility'.\"\n---\n\n# Shopify Embedded App Accessibility (WCAG 2.1 AA)\n\nAccessibility is a Built for Shopify gate and a legal requirement (ADA Title III, European Accessibility Act took effect 2025-06-28). Audited apps fail BFS review when keyboard navigation breaks inside iframes, focus indicators are stripped, alt text is missing on product imagery, or color contrast drops below 4.5:1. This skill is the checklist for every embedded app feature.\n\n## 1. When to Use\n\nTrigger this skill when:\n\n- Building any new screen, Modal, SaveBar, or form in an embedded app\n- Auditing an existing app before Built for Shopify submission\n- Investigating a merchant complaint about keyboard or screen reader usage\n- Reviewing a PR that touches focus, ARIA, color, or alt text\n- Deciding whether to override a Polaris token (almost never — see Section 6)\n- Adding i18n / RTL support that touches reading order, aria-label strings, or lang attributes\n- Pairing with `polaris-ui` (component usage) or `app-bridge` (Modal/SaveBar) — this skill is the a11y overlay on top of both\n\nDo NOT use this skill for storefront / theme accessibility — that is a separate domain (`shopify.dev/docs/storefronts/themes/best-practices/accessibility`).\n\n## 2. WCAG 2.1 AA Criteria Applied to Shopify Apps\n\nShopify Polaris targets WCAG 2.1 A and AA by default. Embedded apps inherit that baseline only if you use Polaris components correctly and do not override semantics, contrast, or focus. The criteria that matter most for admin apps:\n\n**Perceivable**\n\n- 1.1.1 Non-text Content (A) — every image, icon-only button, and chart needs a text alternative\n- 1.3.1 Info and Relationships (A) — use semantic HTML; `<Text as=\"h2\">` not `<Text variant=\"headingMd\">` on a paragraph\n- 1.3.5 Identify Input Purpose (AA) — use `autoComplete` on email, name, address, tel inputs\n- 1.4.3 Contrast Minimum (AA) — 4.5:1 for text under 18pt, 3:1 for large text\n- 1.4.11 Non-text Contrast (AA) — 3:1 for form borders, focus rings, icons that convey state\n- 1.4.10 Reflow (AA) — content must reflow at 320 CSS px width; no horizontal scroll inside the iframe\n- 1.4.12 Text Spacing (AA) — line height 1.5x font-size, paragraph spacing 2x; Polaris defaults pass\n\n**Operable**\n\n- 2.1.1 Keyboard (A) — every interactive element reachable and operable via keyboard\n- 2.1.2 No Keyboard Trap (A) — except in Modal, focus must be able to leave any region\n- 2.4.3 Focus Order (A) — DOM order matches visual order; do not reorder with positive `tabindex`\n- 2.4.7 Focus Visible (AA) — never set `outline: none` without a replacement\n- 2.5.5 Target Size (AAA, but BFS-watched) — interactive targets at least 44x44 CSS px\n\n**Understandable**\n\n- 3.1.1 Language of Page (A) — `<html lang=\"en\">` set at the embedded app's HTML root, not the Shopify admin's\n- 3.2.2 On Input (A) — changing a `<Select>` value must not auto-submit or auto-navigate\n- 3.3.1 Error Identification (A) — TextField `error` prop populated; never rely on red border alone\n- 3.3.2 Labels or Instructions (A) — every TextField has a non-empty `label`\n\n**Robust**\n\n- 4.1.2 Name, Role, Value (A) — custom widgets must expose accessible name + role + state\n- 4.1.3 Status Messages (AA) — Toasts and Banners must be announced (Polaris Toast uses `role=\"status\"` automatically)\n\n## 3. Top 20 Accessibility Failures in Embedded Apps\n\n| # | Failure | Fix |\n|---|---------|-----|\n| 1 | Icon-only Button with no label | `<Button icon={DeleteIcon} accessibilityLabel=\"Delete product\" />` |\n| 2 | Image with empty/missing alt | `<img src={url} alt=\"Product hero photo of red sneaker\" />` or `alt=\"\"` for decoration |\n| 3 | TextField with no `label` prop | Always set `label`; use `labelHidden` if hiding visually |\n| 4 | `<div onClick>` for interactive element | Use `<Button variant=\"plain\">` instead |\n| 5 | Color alone conveys state (red border = error) | Pair color with `error=\"Email is required\"` text + icon |\n| 6 | Modal opens but focus stays on trigger | Polaris Modal handles this — do not override; do not roll your own |\n| 7 | Modal closes but focus does not return | Same — let Polaris Modal manage it; never call `.focus()` manually inside `onClose` |\n| 8 | `outline: none` stripped from focus ring | Remove the rule; or replace with `outline: 2px solid var(--p-color-border-focus); outline-offset: 2px;` |\n| 9 | Positive `tabindex=\"3\"` to reorder | Delete the tabindex; restructure DOM |\n| 10 | `<h1>` then `<h4>` (skipped heading levels) | Use `<Text as=\"h1\">`, `<Text as=\"h2\">` in DOM order |\n| 11 | Toast for a critical error message | Use `<Banner tone=\"critical\">` (persistent) instead — Toasts auto-dismiss |\n| 12 | Loading spinner with no accessible name | `<Spinner accessibilityLabel=\"Loading products\" />` |\n| 13 | Tooltip is the only place help text lives | Move to `helpText` prop on TextField; tooltip can supplement |\n| 14 | IndexTable row click but no row label | `<IndexTable.Row id={id} position={i}>` plus row content with names |\n| 15 | Form submits on Enter but no submit button | Add a visible `<Button submit>Save</Button>` for screen reader users |\n| 16 | Color contrast 3.5:1 on subdued text | Use `tone=\"subdued\"` only on small non-essential text; never below 4.5:1 for body |\n| 17 | Animated Banner / Toast with no `prefers-reduced-motion` respect | Polaris respects it; do not add your own keyframe animations |\n| 18 | ResourcePicker invoked from a non-button element | Trigger from `<Button>` so screen reader announces it as a button |\n| 19 | iframe missing `title` attribute | `<iframe title=\"Shopify product import preview\" ...>` |\n| 20 | App HTML has no `lang` attribute | Add `<html lang=\"en\">` (or merchant's locale) to the embedded app root |\n\n## 4. Keyboard Navigation\n\n### Focus Trap in Modal\n\nPolaris `<Modal>` and App Bridge `<ui-modal>` both implement focus trap automatically. When the modal opens, focus moves to the modal container (or the first focusable element). Tab cycles through focusable elements inside the modal only. Shift+Tab cycles backward. Escape closes the modal. Click on the backdrop closes. Do not write your own focus trap — you will diverge from Polaris and confuse screen readers.\n\n```tsx\nimport { Modal, TextField } from '@shopify/polaris';\n\n<Modal\n open={isOpen}\n onClose={() => setIsOpen(false)}\n title=\"Edit Product\"\n primaryAction={{ content: 'Save', onAction: handleSave }}\n secondaryActions={[{ content: 'Cancel', onAction: () => setIsOpen(false) }]}\n>\n <Modal.Section>\n <TextField label=\"Product name\" value={name} onChange={setName} autoFocus />\n </Modal.Section>\n</Modal>\n```\n\nNotes:\n- `title` becomes `aria-labelledby` automatically\n- `autoFocus` on the first input is allowed and recommended\n- Do NOT add `aria-modal=\"true\"` manually — Polaris already does\n\n### Focus Return on Close\n\nWhen the Modal closes, Polaris returns focus to the trigger element automatically. This works only if:\n\n- The trigger is still mounted (do not unmount the trigger button while Modal is closing)\n- You did not call `.blur()` on the trigger before opening the Modal\n- You did not move focus elsewhere in `onClose`\n\nIf the trigger is dynamic (inside a table row that may have unmounted), use a `useRef` to a stable wrapper:\n\n```tsx\nconst triggerRef = useRef<HTMLButtonElement>(null);\n// pass triggerRef.current to focus on close if needed\n```\n\n### Save Bar Focus\n\nApp Bridge `<ui-save-bar>` and Polaris `<ContextualSaveBar>` are not modal — they do not trap focus. Save / Discard are reachable via Tab from the form. Two rules:\n\n1. The save bar's primary action must be reachable without leaving the form (do not place the save bar in a portal that breaks Tab order)\n2. After Save success, focus should return to the form area (not jump to `<body>`). If you re-render the page, place focus on the page heading:\n\n```tsx\nconst headingRef = useRef<HTMLHeadingElement>(null);\n\nconst handleSaveSuccess = () => {\n shopify.toast({ title: 'Saved' });\n headingRef.current?.focus();\n};\n\n<h1 ref={headingRef} tabIndex={-1}>Product settings</h1>\n```\n\nThe `tabIndex={-1}` makes it programmatically focusable without inserting it into the Tab order.\n\n### Resource Picker Focus\n\n`shopify.resourcePicker()` and `<ui-resource-picker>` are full-screen overlays rendered by the Shopify admin (not your iframe). Shopify handles focus inside the picker. Your responsibility:\n\n- The trigger must be a `<Button>` (announced as button by screen readers)\n- After `onSelection` or `onCancel`, focus should return to the trigger; Shopify usually does this — if not, focus the trigger manually\n\n```tsx\nconst pickerTriggerRef = useRef<HTMLButtonElement>(null);\n\nconst openPicker = async () => {\n await shopify.resourcePicker({\n type: 'product',\n onSelection: (resources) => {\n setSelected(resources.selection);\n pickerTriggerRef.current?.focus();\n },\n onCancel: () => pickerTriggerRef.current?.focus(),\n });\n};\n```\n\n### Tab Order Rules\n\n- Visual order must match DOM order\n- Never use `tabindex` greater than 0\n- `tabindex=\"0\"` to make a non-interactive element focusable (rare — use a Button instead)\n- `tabindex=\"-1\"` to make an element programmatically focusable but skipped by Tab (page headings after navigation)\n\n## 5. Screen Reader Testing\n\nTest every release on at least one screen reader. Two minimum combos cover ~80% of the market:\n\n| Screen Reader | OS | Browser | Cost |\n|---------------|-----|---------|------|\n| NVDA | Windows | Firefox or Chrome | Free |\n| VoiceOver | macOS | Safari | Built in |\n| JAWS | Windows | Chrome | Paid (skip unless enterprise) |\n\n**Minimum coverage: NVDA on Firefox AND VoiceOver on Safari.**\n\n### What to Test\n\nFor every page or modal:\n\n1. Page load — does the screen reader announce the page heading?\n2. Heading navigation (H key in NVDA, VO+Cmd+H in VoiceOver) — heading levels are sensible and ordered\n3. Form fields — each TextField announces its label, required state, error message, help text\n4. Buttons — every button has a non-empty accessible name (no \"Button\" alone)\n5. Tables — IndexTable rows announce row position and content\n6. Modal open — title is announced, focus is inside the modal, Escape closes\n7. Toast — success messages are announced; critical errors use Banner instead\n8. Live regions — Banner / Toast updates do not interrupt the user mid-sentence\n\n### Quick NVDA Cheat Sheet\n\n- Start: `Ctrl + Alt + N`\n- Stop speech: `Ctrl`\n- Read next line: `Down`\n- Read element: `NVDA + Tab`\n- Headings list: `Insert + F7`\n- Browse mode toggle: `NVDA + Space`\n\n### Quick VoiceOver Cheat Sheet\n\n- Start / stop: `Cmd + F5`\n- VO key: `Ctrl + Option`\n- Move next: `VO + Right`\n- Open rotor (headings, links, forms): `VO + U`\n- Read element: `VO + A`\n\n## 6. Polaris Color Contrast Tokens\n\nPolaris colors are generated in HSLuv to guarantee WCAG 2.1 AA contrast. Use semantic tokens. Never hardcode hex values for text or interactive elements.\n\n### Safe text tokens (4.5:1+ against `bg-surface`)\n\n- `--p-color-text` — primary body text\n- `--p-color-text-subdued` — secondary text; still passes 4.5:1 on bg-surface but fails on bg-surface-secondary in some themes — measure\n- `--p-color-text-critical` — error text\n- `--p-color-text-success` — success text\n- `--p-color-text-warning` — warning text\n- `--p-color-text-inverse` — text on dark fills\n\n### Safe interactive tokens (3:1+ for borders and icons)\n\n- `--p-color-border` — form borders\n- `--p-color-border-focus` — focus rings\n- `--p-color-icon` — neutral icons\n- `--p-color-icon-subdued` — only on icons that have a visible text label nearby\n\n### Tokens to NEVER override below contrast\n\n| Token | Minimum ratio | Notes |\n|-------|---------------|-------|\n| `--p-color-text` | 4.5:1 | Body copy. Override only if you re-measure. |\n| `--p-color-text-critical` | 4.5:1 | Error messages. |\n| `--p-color-border-focus` | 3:1 | Focus ring against surface AND against fill — must pass on both. |\n| `--p-color-border` | 3:1 | Form borders against the surface they sit on. |\n\n### When You Must Use Brand Color\n\nIf product owner wants a custom brand color on a Button or Banner accent:\n\n1. Run the candidate color through a contrast checker against `--p-color-bg-surface` (light) AND its dark theme equivalent if Shopify admin dark mode is enabled\n2. Aim for 4.5:1 on text, 3:1 on borders / icons\n3. If it fails, darken until it passes — do not ship a failing color\n\nPolaris ships some yellow tones that are marginal on light backgrounds. For warning text always use `--p-color-text-warning`, not raw yellow hex.\n\n## 7. ARIA — When Needed, When Polaris Covers It\n\nThe first rule of ARIA: do not use ARIA if a native HTML element exists. The second rule: if Polaris renders an element, it already wires the ARIA. Do not double-aria.\n\n### Already Handled by Polaris (do NOT add yourself)\n\n| Polaris Component | ARIA it sets | Do not duplicate |\n|-------------------|--------------|------------------|\n| `<Modal title=\"X\">` | `role=\"dialog\"`, `aria-modal=\"true\"`, `aria-labelledby` | Adding `role=\"dialog\"` to a child div is wrong |\n| `<TextField label=\"X\" error=\"Y\">` | `aria-labelledby`, `aria-invalid`, `aria-describedby` | Adding `aria-label` on top of `label` is wrong |\n| `<Button>` | `role=\"button\"` (it IS a button) | Adding `role=\"button\"` to a Polaris Button is redundant |\n| `<Banner>` | `role=\"status\"` or `role=\"alert\"` based on tone | Adding `aria-live` manually breaks announcements |\n| `<Toast>` | `role=\"status\"` + live region | Do not wrap in your own `aria-live` |\n| `<Tabs>` | `role=\"tablist\"`, `role=\"tab\"`, `aria-selected`, `aria-controls` | Hand-rolled tabs are an anti-pattern |\n| `<Checkbox>` / `<RadioButton>` | `role=\"checkbox\"`/`\"radio\"`, `aria-checked` | Use the component |\n| `<Spinner accessibilityLabel=\"X\">` | `role=\"status\"`, `aria-label` | Always provide accessibilityLabel |\n\n### Where You Must Add ARIA Yourself\n\n1. **Icon-only Buttons** — `accessibilityLabel` prop:\n ```tsx\n <Button icon={DeleteIcon} accessibilityLabel=\"Delete product\" />\n ```\n2. **ResourceItem** — `accessibilityLabel` prop describing the row action:\n ```tsx\n <ResourceItem id={id} accessibilityLabel={`View ${product.title}`} />\n ```\n3. **Custom widgets** — if you must build a custom dropdown, accordion, or combobox (consider redesigning with Polaris first), follow the WAI-ARIA Authoring Practices pattern exactly\n4. **Page heading after route change** in SPA — set `tabIndex={-1}` and call `.focus()`\n5. **Decorative images** — `alt=\"\"` (empty alt, never missing)\n6. **Informative images** — `alt=\"Descriptive sentence under 125 chars\"`\n7. **iframes you create** — `title=\"...\"` (browser screen readers announce this)\n\n## 8. Internationalization + Accessibility\n\nEmbedded apps support multiple merchant locales. Accessibility strings localize too.\n\n### `lang` Attribute\n\nSet on the embedded app's HTML root, matching the merchant's locale (you can read it from `shopify.config.locale` or the `locale` URL param):\n\n```html\n<html lang=\"fr-CA\">\n```\n\nIf a section of content is in a different language than the page, mark it:\n\n```html\n<p>The product is <span lang=\"ja\">商品</span>.</p>\n```\n\nThis lets screen readers switch pronunciation engine.\n\n### RTL Support\n\nFor Arabic and Hebrew merchants, set `dir=\"rtl\"` on the HTML root. Polaris supports RTL via the `AppProvider`'s i18n object. Mirror:\n\n- Layout direction (Polaris handles this in CSS)\n- Icons that imply direction (`ChevronRightIcon` should flip to `ChevronLeftIcon`)\n- Reading order in custom layouts\n\n### Localized Accessibility Strings\n\n`accessibilityLabel`, `helpText`, `error`, `label` props all accept strings — pass them through your i18n library:\n\n```tsx\n<Button icon={DeleteIcon} accessibilityLabel={t('product.delete.label')} />\n<TextField label={t('product.name.label')} helpText={t('product.name.help')} />\n```\n\nNever concatenate strings (\"Delete \" + product.title) — pluralization and word order vary by locale. Use ICU message format.\n\n### Locale-aware Screen Reader Strings\n\nFor Toasts and Banners, run the message through i18n before passing to `shopify.toast({ title, message })`. The admin's screen reader uses the merchant's locale; mismatched language makes the announcement unintelligible.\n\n## 9. Built for Shopify Accessibility Gates\n\nBuilt for Shopify certification reviews accessibility. As of the 2026 review criteria the gates that block approval are:\n\n1. **Keyboard navigation works on every interactive element** — Tab reaches everything, Enter / Space activates, Escape closes overlays\n2. **Visible focus indicator on every focusable element** — no `outline: none` without a replacement\n3. **Color contrast 4.5:1 on text, 3:1 on UI components** — measured at the default Shopify admin theme\n4. **All form inputs have labels** — visible or `labelHidden`, never absent\n5. **Modal focus management works** — focus enters on open, traps inside, returns on close\n6. **Icon-only buttons have accessible names** — `accessibilityLabel` populated\n7. **Images have alt text or `alt=\"\"`** — never missing\n8. **No keyboard traps outside Modal** — Tab can always leave a region\n9. **Page has a `<h1>`** and heading levels are not skipped\n10. **Status messages are announced** — Toasts and Banners use Polaris components, not custom `<div>`\n\nBFS auditors test with NVDA + Firefox and VoiceOver + Safari. Apps fail review if they break these on the primary user flows (install, onboard, core feature).\n\nAlways re-check the current criteria at shopify.dev/docs/apps/launch/built-for-shopify before submitting — Shopify updates the bar.\n\n## 10. Common Fixes Table\n\n| Issue | Fix Code |\n|-------|----------|\n| Icon-only Button | `<Button icon={EditIcon} accessibilityLabel=\"Edit product\" />` |\n| Decorative image | `<img src=\"...\" alt=\"\" role=\"presentation\" />` |\n| Informative image | `<img src=\"...\" alt=\"Customer order shipped in branded box\" />` |\n| TextField needs hidden label | `<TextField label=\"Search\" labelHidden value={q} onChange={setQ} />` |\n| Page heading after route change | `<h1 ref={r} tabIndex={-1}>Title</h1>` then `r.current?.focus()` |\n| Replace `outline: none` | `outline: 2px solid var(--p-color-border-focus); outline-offset: 2px;` |\n| Loading spinner | `<Spinner accessibilityLabel=\"Loading orders\" size=\"large\" />` |\n| Error not visible to SR | `<TextField label=\"Email\" error=\"Email is required\" value={v} onChange={setV} />` |\n| Custom dropdown | Replace with `<Select>` or `<Combobox>` from Polaris |\n| Modal without title | Always pass `title` prop — used for `aria-labelledby` |\n| Toast for critical error | Replace with `<Banner tone=\"critical\" title=\"...\">...</Banner>` |\n| Heading level skip | Use `<Text as=\"h2\">`, `<Text as=\"h3\">` in order |\n| ResourceItem unclear | `<ResourceItem id={id} accessibilityLabel={`Open order ${order.name}`} />` |\n| iframe missing title | `<iframe title=\"Product preview\" src={url} />` |\n| Form has no submit button | Add `<Button submit>Save</Button>` even if SaveBar exists |\n| Color-only state on Badge | `<Badge tone=\"critical\">Failed</Badge>` (text + tone, not tone alone) |\n| HTML root no lang | `<html lang={merchantLocale}>` at template / index.html |\n| Decoration icon announced | `<Icon source={DotIcon} accessibilityLabel=\"\" />` or wrap with `aria-hidden=\"true\"` |\n| Long form needs structure | Use `<FormLayout.Group>` to group related fields |\n| Disabled button with no reason | Add `helpText` near the trigger explaining why |\n\n## 11. Twenty A11y Rules for Our Plugin\n\n1. Wrap every embedded app in `<AppProvider i18n={...}>` and `<Frame>` if you need toasts and loading\n2. Use Polaris components for every interactive element — Button, TextField, Select, Modal, Banner, Toast\n3. Never set `outline: none` on any focusable element without an equivalent visible replacement\n4. Every Button that has only an icon must set `accessibilityLabel`\n5. Every TextField, Select, Checkbox, RadioButton must have a non-empty `label` (use `labelHidden` to hide visually)\n6. Every image gets an `alt` attribute — `alt=\"\"` for decoration, descriptive sentence for content\n7. Use semantic heading levels — `<Text as=\"h1\">` once per page, then h2, h3 in order\n8. Use `<Banner tone=\"critical\">` for persistent critical errors, `<Toast>` for transient success\n9. Validate forms inline with the `error` prop; never rely on color alone\n10. Test color contrast at 4.5:1 minimum for text; use Polaris tokens, never hardcode hex for text\n11. Trigger Modal from a `<Button>`; let Polaris handle focus trap and return\n12. Trigger ResourcePicker from a `<Button>`; restore focus to trigger after `onSelection` / `onCancel`\n13. After SPA navigation, focus the new page heading with `tabIndex={-1}` + `.focus()`\n14. Set `lang` on the HTML root matching the merchant's locale\n15. Localize every user-facing string including `accessibilityLabel`, `helpText`, `error`\n16. Never use a positive `tabindex` value; never reorder DOM with CSS in a way that breaks Tab order\n17. Test every release with NVDA on Firefox AND VoiceOver on Safari\n18. Run `axe-core` or `@axe-core/react` in development; fail CI on critical violations\n19. Document any Polaris override in a comment with the contrast ratio measured\n20. Reserve `aria-*` attributes for cases Polaris does not cover; never double-aria a Polaris element\n\n## 12. Test Checklist\n\nPre-merge a11y checklist:\n\n- [ ] All interactive elements reachable via Tab in DOM order\n- [ ] Visible focus indicator on every focusable element\n- [ ] Escape closes every Modal\n- [ ] Focus returns to trigger after Modal closes\n- [ ] Every Button with an icon-only has `accessibilityLabel`\n- [ ] Every TextField / Select / Checkbox has a `label`\n- [ ] Every image has `alt` (empty for decoration, descriptive otherwise)\n- [ ] No `outline: none` without replacement\n- [ ] Page has exactly one `<h1>` and no skipped heading levels\n- [ ] Color contrast 4.5:1 on text, 3:1 on borders / icons (sampled with browser devtools)\n- [ ] Forms validate inline with `error` prop, not color alone\n- [ ] Critical errors use `<Banner>`, not `<Toast>`\n- [ ] `lang` attribute set on HTML root\n- [ ] axe-core run shows zero critical or serious violations\n- [ ] Manual NVDA + Firefox pass on the core happy path\n- [ ] Manual VoiceOver + Safari pass on the core happy path\n- [ ] Keyboard-only walkthrough of install, onboard, and primary feature\n- [ ] Screen at 320 CSS px width — no horizontal scroll inside the iframe\n- [ ] Text resize at 200% — no clipped content\n- [ ] `prefers-reduced-motion` respected (Polaris does this by default — verify no custom keyframes)\n- [ ] Every accessibilityLabel and helpText is in the merchant's locale\n\n## 13. Decision Tree\n\n```\nBuilding a new UI element?\n│\n├── Is there a Polaris component for it?\n│ ├── Yes → Use Polaris. Pass label / accessibilityLabel / helpText / error props. STOP.\n│ └── No → Continue.\n│\n├── Is there a native HTML element for it (button, a, input, label, table, dialog)?\n│ ├── Yes → Use native HTML. Add ARIA only if native semantics insufficient.\n│ └── No → Continue.\n│\n├── Are you building a custom widget (combobox, accordion, tabs)?\n│ ├── Yes → Look one more time for a Polaris equivalent. If genuinely none:\n│ │ follow WAI-ARIA Authoring Practices pattern exactly,\n│ │ write keyboard handlers, test with NVDA + VoiceOver,\n│ │ document the ARIA contract in the component header\n│ └── No → Reconsider; you probably do not need a new widget\n│\nAfter build:\n│\n├── Run axe-core. Zero critical / serious violations? → Continue. Else fix.\n├── Tab through entire flow with no mouse. Reach everything? → Continue. Else fix focus order.\n├── Open NVDA + Firefox. Every Button, Field, Heading announced clearly? → Continue. Else add accessibilityLabel / fix heading.\n├── Open VoiceOver + Safari. Same pass. → Continue. Else fix.\n├── Sample colors with devtools. 4.5:1 text, 3:1 borders? → Continue. Else swap to Polaris token.\n└── Resize to 320 px wide and 200% zoom. No clipping? → Ship. Else use Polaris layout primitives (BlockStack, InlineStack, InlineGrid) to fix reflow.\n```\n\n## Sources\n\n- [Polaris Accessibility — Shopify Polaris React](https://polaris-react.shopify.com/foundations/accessibility)\n- [Accessibility testing — Shopify/polaris (GitHub)](https://github.com/Shopify/polaris/blob/main/documentation/Accessibility%20testing.md)\n- [Accessibility best practices for Shopify apps — shopify.dev](https://shopify.dev/docs/apps/build/accessibility)\n- [ui-modal — App Bridge Library](https://shopify.dev/docs/api/app-bridge-library/web-components/ui-modal)\n- [Polaris Color Tokens](https://polaris-react.shopify.com/tokens/color)\n- [Screen Reader Testing Guide — TestParty](https://testparty.ai/blog/screen-reader-testing-guide)\n"
}SHA-256 of public snapshot: 9e31369351b754d30eedf803c0871291a601f8069ffefd32523732bf1ca6f333