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:
- Press Tab as the first action on page load: a "Skip to content" link must
become visible and, when activated, move focus to
<main>. - Tab through the entire page: every link, button, and form control must receive visible focus in a logical order; no element may trap focus.
- Open a modal (mini-cart, size guide): focus must move into it, Escape must close it, and focus must return to the trigger.
- 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.