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:
A code block pushed the page sideways at 390px
A grid item defaults to
min-width: autoand 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.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 failed | Cause | Now prevented by |
|---|---|---|
| Primary button at 4.25:1 | A class-merge utility read a size token as a colour and dropped text-white | Custom scale registered; regression test |
| Dim text failed on one surface | The guarantee checked one background, not all of them | Every text token asserted against every surface |
| Heading at 20px in accent | font-weight: 600 is not bold, so the large-text exemption did not apply | Accent display floor set to 24px |
| Ghost numeral at 1.15:1 | Decorative giant number behind a title, marked aria-hidden | Removed — see below |
| The same defect again at 1.43:1 | Re-introduced months later on another page | Measured value in the code comment |
| Search label at 70% of a token | The dimmest passing token has no headroom for a fraction | Full-strength token only |
| Mobile menu unusable when scrolled | backdrop-filter made the header a containing block for its fixed child | Test opens the menu at scroll 2600 |
| Escape never closed the menu in Safari | Safari does not focus a <button> on click, so a container-scoped handler never fired | WebKit in the e2e matrix |
| Focus trap leaked | Boundary-only trapping is unreliable | Every Tab is driven in test |
| Two touch targets under 44px | Icon-only links sized to their glyph | Asserted across every interactive element |
| Table scroll region unreachable | Overflow container with no focusable child | Labelled <section> + tabIndex |
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.
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.
A container that scrolls must be reachable by keyboard
Wide content scrolls in its own
overflow-x: auto, and that container needstabIndex={0}and a label — unless it contains focusable children, in which case tabbing already scrolls it into view.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.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.
Announce a shortcut rather than hiding it
A
/-to-search hint markedaria-hiddenkeeps genuinely useful information from assistive tech.aria-keyshortcutson 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:
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.
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.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.
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
axe on every route, at two widths, failing the build
One width hides overflow and scroll-region defects entirely.
Run Lighthouse too, and treat a disagreement as a finding
It does not emulate reduced motion, so it sees things axe structurally cannot.
Compute contrast, and check every text token against every surface
Not against the one it was designed on.
Test the keyboard path, not just the markup
Drive every Tab. Include WebKit — its focus behaviour genuinely differs.
Assert touch targets at 44px across all interactive elements
Icon-only controls are where this fails.
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.