Skip to content

Writing

Web accessibility as a competitive advantage: an engineering checklist

The accessibility defects an automated suite finds, the ones it structurally cannot, and eleven real failures from one build — what caused each, and the check that now prevents it.

7 min read

Accessibility is usually argued for on ethical or legal grounds, and both arguments are sound. This post makes a narrower one: the work that makes a site accessible is the same work that makes it correct, and a team that does it finds defects that would otherwise ship as ordinary bugs.

Every failure below is real, from a single project. None was found by a person clicking around.

What an automated suite gives you, and what it cannot

Run axe on every route at multiple widths, in CI, failing the build. It is the cheapest useful thing you can do and it catches a genuine category of defect.

It also has two structural blind spots that matter more than its false negatives.

The second blind spot is width. A container only overflows at the width where it actually overflows, which is never the width anyone checks:

  1. A code block pushed the page sideways at 390px

    A grid item defaults to min-width: auto and refuses to shrink below its content's min-content width — very wide for a <pre> of long lines. 353px of horizontal overflow at 390px, and none at 1440.

  2. A terminal block was not keyboard-reachable, also only at 390px

    It scrolls horizontally, and a scrolling container needs to be focusable. At desktop width it did not scroll, so the rule never fired.

Run the suite at at least two widths. Both defects above were invisible at one.

Eleven real failures, and what caused each

What failedCauseNow prevented by
Primary button at 4.25:1A class-merge utility read a size token as a colour and dropped text-whiteCustom scale registered; regression test
Dim text failed on one surfaceThe guarantee checked one background, not all of themEvery text token asserted against every surface
Heading at 20px in accentfont-weight: 600 is not bold, so the large-text exemption did not applyAccent display floor set to 24px
Ghost numeral at 1.15:1Decorative giant number behind a title, marked aria-hiddenRemoved — see below
The same defect again at 1.43:1Re-introduced months later on another pageMeasured value in the code comment
Search label at 70% of a tokenThe dimmest passing token has no headroom for a fractionFull-strength token only
Mobile menu unusable when scrolledbackdrop-filter made the header a containing block for its fixed childTest opens the menu at scroll 2600
Escape never closed the menu in SafariSafari does not focus a <button> on click, so a container-scoped handler never firedWebKit in the e2e matrix
Focus trap leakedBoundary-only trapping is unreliableEvery Tab is driven in test
Two touch targets under 44pxIcon-only links sized to their glyphAsserted across every interactive element
Table scroll region unreachableOverflow container with no focusable childLabelled <section> + tabIndex
Every one of these rendered correctly and looked fine. That is what makes them worth listing.

aria-hidden is not a contrast exemption

This one is worth its own heading because we made it twice.

A large, very faint numeral sits behind a heading as decoration. It is marked aria-hidden, so a screen reader ignores it — and the reasoning goes that since it is decorative, contrast does not apply.

We removed the first one and then re-introduced the same pattern on a different page months later, at 1.43:1. The fix the second time was to make it legible at a measured 3.69:1 — and to put that measurement in the comment, so the next person to touch it knows it was chosen rather than picked.

The rules that prevent whole categories

Some checks are worth writing as rules rather than as tests, because they apply everywhere.

  1. An inline link is underlined at rest, never on hover alone

    A link inside a paragraph distinguished only by colour fails 1.4.1 and is genuinely invisible to anyone who does not separate the two greys. Standalone links in a nav or a card are exempt — their position marks them.

  2. A container that scrolls must be reachable by keyboard

    Wide content scrolls in its own overflow-x: auto, and that container needs tabIndex={0} and a label — unless it contains focusable children, in which case tabbing already scrolls it into view.

  3. Prefer the element over the ARIA

    A <details> is a disclosure with no ARIA at all, in the tab order, openable by find-in-page. A <section aria-label> beats a <div role="region"> — same landmark, one fewer attribute, and it is what the element is.

  4. Reduced motion means none, not faster

    And the base rule must be the animation's finished state, or somebody with motion off gets a blank page.

  5. Announce a shortcut rather than hiding it

    A /-to-search hint marked aria-hidden keeps genuinely useful information from assistive tech. aria-keyshortcuts on the input tells them properly.

Focus: the state everyone styles away

Two failures here are near-universal.

Removing the indicator. outline: none with nothing in its place fails 2.4.7 outright. If a control's default ring is wrong for it, replace it — do not delete it.

Assuming :focus-visible distinguishes mouse from keyboard. It does for buttons and links. It does not for text inputs: browsers match :focus-visible on them whatever the input modality, because typing is expected. So "show the ring only for keyboard users" cannot be implemented that way on a search field. Change the indicator's shape instead — a border colour change at 3:1 satisfies 2.4.11 without drawing a second box.

Where the advantage actually is

The commercial argument is usually made about market reach or legal exposure. In practice the return shows up somewhere less obvious:

  1. It finds real bugs

    Of the eleven failures above, at least four — the Safari focus bug, the containing-block menu bug, the dropped class, the mobile overflow — were ordinary defects that broke the site for everyone. Accessibility testing is where they surfaced.

  2. It forces semantic markup, which is cheaper to maintain

    A <details> accordion has no state, no ARIA and no handler. The accessible version is smaller than the inaccessible one.

  3. It gives you a contrast system rather than a palette

    Computing ratios means every colour decision has a check attached, which is the same machinery that stops a rebrand shipping an unreadable button.

  4. It is dramatically cheaper before launch

    Every item in that table took minutes to fix in place. Retrofitting a focus model or a contrast system onto a finished product does not.

The checklist

  1. axe on every route, at two widths, failing the build

    One width hides overflow and scroll-region defects entirely.

  2. Run Lighthouse too, and treat a disagreement as a finding

    It does not emulate reduced motion, so it sees things axe structurally cannot.

  3. Compute contrast, and check every text token against every surface

    Not against the one it was designed on.

  4. Test the keyboard path, not just the markup

    Drive every Tab. Include WebKit — its focus behaviour genuinely differs.

  5. Assert touch targets at 44px across all interactive elements

    Icon-only controls are where this fails.

  6. Treat aria-hidden as an AT instruction, never a visual exemption

    If a sighted reader can see it, it has to meet contrast.

Start here

Tell us what you are building

Or what is breaking, or what has to go faster. You will get a straight answer from an engineer who would do the work.