Skip to main content
Version: next

Accessibility audit rules

Audit rules covering landmarks, labels, contrast, and keyboard navigation in a Front-Commerce project theme.

Accessibility issues in a Front-Commerce project almost always live in the integrator's theme overrides and custom components: the base theme ships with sensible semantics, but overridden layouts and new components frequently drop them. These rules focus on the defects most often found in real audits: missing landmarks, unlabeled form fields, and interactive components that only work with a mouse.

AUDIT-A11Y-01 — Structure every page with HTML5 landmarks

Severity: important — Detection: static

Rule: Every page renders exactly one <main> element, a <nav> for the primary navigation, a <footer>, and <aside> for complementary content such as filter sidebars.

Why: Screen reader users navigate by landmark. Without a <main>, they must traverse the header and navigation on every page load to reach the content. Missing <main> and <aside> elements are among the most frequent findings in Front-Commerce audits, because overridden layouts replace them with plain <div> elements.

How to check: Inspect the layout routes and overridden layout components:

grep -rn "<main" app/ extensions/ --include="*.tsx" --include="*.jsx"
grep -rn "<aside\|<nav\|<footer" app/ extensions/ --include="*.tsx" --include="*.jsx"

A violation is any layout route (app/routes/_main.tsx or an overridden layout component) whose rendered tree contains no <main>, or a filter/facet sidebar component rendered as a <div> instead of <aside>. If the project overrides the base theme layout, the override must preserve the landmarks the original provided. Confirm at runtime that each page type (home, category listing, product) exposes exactly one <main>: duplicated landmarks are as confusing as missing ones.

AUDIT-A11Y-02 — Keep a correct heading hierarchy on every page type

Severity: important — Detection: runtime

Rule: Each page type (home, category listing, product, CMS) has exactly one <h1> describing the page, and heading levels descend without skipping (h1 then h2 then h3).

Why: Headings are the second navigation mechanism for screen reader users after landmarks. A page with multiple <h1> elements, or with headings chosen for their font size rather than their level, produces an outline that no longer describes the content. On e-commerce pages this typically breaks on product pages, where the product name, upsell blocks, and reviews compete for heading levels.

How to check: With the application running, extract the heading outline of one URL per page type:

curl -s https://staging.example.com/ | grep -oE "<h[1-6][^>]*>[^<]*" | head -30

Or use the browser devtools accessibility tree, or the HeadingsMap browser extension. A violation is: zero or multiple <h1> elements, a skipped level (an <h3> with no preceding <h2> in its section), or an <h1> that does not describe the page (for example a site-wide logo heading). Check at minimum the home page, one category page, and one product page.

AUDIT-A11Y-03 — Associate a label with every form field

Severity: important — Detection: static

Rule: Every <input>, <select>, and <textarea> in custom theme code has an associated <label htmlFor>, a wrapping <label>, or an aria-label.

Why: An unlabeled field is announced as "edit text" with no purpose, which makes checkout and account forms unusable with a screen reader. This is a recurring real-world finding: quantity inputs on product pages (input#quantity) are frequently shipped without any label because their purpose seems visually obvious.

How to check: List raw form controls in the integrator's code and verify each one:

grep -rn "<input\|<select\|<textarea" app/ extensions/ --include="*.tsx" --include="*.jsx"

For each hit, a violation is a control with no id matched by a htmlFor, not wrapped in a <label>, and without aria-label or aria-labelledby. Pay specific attention to quantity selectors, search fields, and newsletter inputs — the fields whose label is usually "implied" by an adjacent icon or button. Components built on the Front-Commerce Form atoms inherit labels; the risk is in hand-rolled controls. Enabling eslint-plugin-jsx-a11y (label-has-associated-control) makes this check permanent.

AUDIT-A11Y-04 — Give every icon-only control an accessible name

Severity: important — Detection: static

Rule: Every button or link whose only visible content is an icon exposes an accessible name through aria-label or visually hidden text; title alone is not sufficient.

Why: Icon-only controls (cart, wishlist, quantity increment, close buttons) are announced as "button" with no name. title is inconsistently announced by screen readers (NVDA and VoiceOver often skip it), so it cannot be the only mechanism. An aria-label also doubles as a robust test selector through getByRole(..., { name }), removing the need for data-testid.

How to check: Find icon-only controls in custom components:

grep -rn -B2 -A2 "<Icon " app/ extensions/ --include="*.tsx" | grep -B3 "</button>\|</Link>"
grep -rn "aria-label" app/ extensions/ --include="*.tsx"

A violation is a <button> or <Link> whose children are only an <Icon> (or SVG) and that has no aria-label. Also flag controls carrying only title or only data-testid. The Icon component's title prop provides an SVG title, but the interactive wrapper still needs its own accessible name.

AUDIT-A11Y-05 — Meet AA contrast, including focus and hover states

Severity: important — Detection: manual

Rule: All text and interactive states meet WCAG 2.1 AA contrast ratios (4.5:1 for normal text, 3:1 for large text and UI component boundaries), including hover, focus, and disabled-looking states.

Why: Automated tools only measure the default state. Real audits regularly find focus outlines or hover colors that fall below 3:1 against their background, which makes keyboard navigation invisible for low-vision users even when the resting design passes.

How to check: Extract the design tokens from the theme:

grep -rn -- "--color\|\\$color" app/theme/ extensions/ --include="*.scss" --include="*.css" | head -40

Test the main text/background and interactive-state pairs with a contrast checker (for example colourcontrast.cc). Then, in a browser, Tab through the header, a product card, and the add-to-cart button while observing the focus indicator: a violation is any focused element whose indicator is below 3:1 contrast against adjacent colors, or that changes only by a color shift below 3:1 on hover. Record the failing color pairs and their computed ratios in the report.

AUDIT-A11Y-06 — Support complete keyboard navigation with visible focus

Severity: important — Detection: runtime

Rule: Every interactive element is reachable and operable with the keyboard alone, a skip-to-content link is present and functional, and focus moves logically when a modal opens or a page changes.

Why: Keyboard access is the foundation for every assistive technology. A single mouse-only widget on the checkout path (a swatch picker, a pickup point map) blocks the purchase entirely for keyboard users.

How to check: With the application running, on the home page, a category page, a product page, and the cart:

  1. Press Tab as the first action on page load: a "Skip to content" link must become visible and, when activated, move focus to <main>.
  2. Tab through the entire page: every link, button, and form control must receive visible focus in a logical order; no element may trap focus.
  3. Open a modal (mini-cart, size guide): focus must move into it, Escape must close it, and focus must return to the trigger.
  4. Operate the add-to-cart flow end to end without a mouse.

A violation is any interactive element skipped by Tab, any invisible focus position, or a missing/broken skip link. Statically, verify the skip link survived layout overrides: grep -rn "skip" app/ --include="*.tsx" -i.

AUDIT-A11Y-07 — Build custom interactive components on correct ARIA patterns

Severity: important — Detection: static

Rule: Custom widgets (accordions, carousels, dropdowns, swatch groups) use the role and state attributes defined by the ARIA Authoring Practices Guide for their pattern, and are operable with the keyboard.

Why: Misused ARIA is worse than missing ARIA — it lies to assistive technology. A swatch group using aria-pressed instead of role="radiogroup", or a disclosure button without aria-expanded and aria-controls, announces a widget that behaves differently from what the user was told.

How to check: Inventory ARIA usage in custom components:

grep -rn "aria-pressed\|aria-expanded\|aria-controls\|role=" app/ extensions/ --include="*.tsx"
grep -rn "onClick" app/ extensions/ --include="*.tsx" | grep -v "button\|Button\|Link\|<a "

Violations: aria-pressed on single-choice groups (use role="radio" inside role="radiogroup"), show/hide triggers without aria-expanded + aria-controls, onClick handlers on non-interactive elements (div, span, li) without a role and keyboard handler, and error messages not linked to their input through aria-invalid + aria-describedby. Enabling eslint-plugin-jsx-a11y rules (role-supports-aria-props, role-has-required-aria-props, no-noninteractive-element-interactions) automates most of this rule.

AUDIT-A11Y-08 — Run automated accessibility audits on key pages

Severity: minor — Detection: runtime

Rule: A Lighthouse accessibility audit scores at least 90 on the home, category, product, and cart pages, and an Axe scan reports no critical or serious violations on those pages.

Why: Automated audits catch roughly a third of accessibility defects (contrast, missing attributes, invalid ARIA) at near-zero cost, and provide a regression baseline. A project with no automated baseline silently regresses with every theme change.

How to check: Against a running instance or deployed URL:

npx lighthouse https://staging.example.com/ --only-categories=accessibility --output=json --quiet
npx @axe-core/cli https://staging.example.com/some-product.html

Run both tools on one URL per page type. A violation is a Lighthouse accessibility score below 90 or any Axe violation of impact critical or serious. Record the failing audit IDs — they usually map directly to one of the rules above. Automated results complement, and never replace, the manual checks in AUDIT-A11Y-05 and AUDIT-A11Y-06.