Colour contrast across a marketing site and an authenticated application
One design token, eighteen failing nodes, and the landing-page CTA
What makes it hard
Thirty-three of forty accessibility tests failed. That reads like a module in trouble; it was two design tokens.
The brand primary was below the 4.5:1 floor for normal text in every single usage:
- white on the primary, which is the signup call to action on the landing page, at 14px: 2.83:1
- badge text on its own tint: 2.50:1
- link text on a near-white card: 2.78:1
Two muted text steps on the dark hero failed as well, at 4.48:1 and 3.15:1 — one of them marginal enough that it would pass on a rounding change and fail again on the next.
Eighteen failing nodes on the landing page alone, and all eighteen were repetitions of those five combinations across repeated cards. The primary call to action on the highest-traffic page in the product was the worst offender.
The reporting shape is the lesson. Thirty-three failures across five feature files looks like thirty-three defects and gets triaged as a suite problem. Every one of them resolved to a single custom property.
How to cover it
Be exact about what SDODS gives you here, because it is a floor rather than a verdict.
@a11yaudits the page a UI scenario ends on with axe-core (WCAG A and AA) and fails on anything at or abovea11y.failOn, which isseriousunless the project says otherwise. The full result is attached assdods/a11y-scenario.the page should have no colour-contrast violationsisolates the contrast rule, so a token change fails on contrast rather than inside a general violation list. It has awithin {string}form for one region.gates.a11yon a process failssdods run --processwhen an audit found a blocking violation, or when no audit ran at all.
What axe will not do for you is group findings or read your design tokens. That part is still project-local.
What the failure shape teaches, which is the transferable part:
- Report by token, not by node. Eighteen nodes that resolve to one custom property is one fix. Have the step group violations by the resolved colour pair before it reports, and the run tells you there are two problems rather than thirty-three.
- A visual baseline will not catch this.
the page should match the visual baseline {string}compares against a screenshot that was captured with the failing colour in it. It is happy forever. Contrast is a computed property, not a pixel diff — this is one of the clearest cases where@visualand@a11yare genuinely different tests. - Push the check left. A contrast assertion belongs in the design-token pipeline, where a failing pair cannot merge. The suite should be the second line, catching a token used in a combination nobody modelled.
- Root-cause the halves separately. The public surfaces were root-caused to the token; the authenticated and admin surfaces, which were the larger half, had not been. Keyboard order, focus visibility and landmark semantics are separate assertions with separate causes, and folding them into one number hides that.
Scenario sketch
@ui @regression @a11y
Scenario: The landing page has no contrast violations
Given I navigate to the "landing" page
Then the page should have no colour-contrast violations
@ui @regression @a11y
Scenario: The primary call to action meets the normal-text threshold
Given I navigate to the "landing" page
Then the element with test id "hero-cta-signup" should meet a contrast ratio of 4.5The first Then is in @sdods/core/steps, and the @a11y tag audits the whole page again at the end of each scenario. The second is project-local: nothing in the shared library asserts a ratio on one element, so write it in your project's steps directory and phrase it in the same style as the shared steps.
Gate on it in CI with a process whose gates include a11y: true, and let sdods run --process fail the run.
Other use cases
- Testing an eight-screen auth funnel that has no test idsLogin, signup, email verification, 2FA, the device flow, OAuth consent, invitation acceptance and password reset
- A 403 that a shared cache is free to replayAuthorisation refusals on an API served through a CDN
- Plan caps, spending limits, and a gate that never firedBilling, usage and spending-limit routes behind plan entitlements
- A workflow canvas SDODS cannot dragA node-graph editor: 193 block types, edges, handles, sub-block inputs and subflow containers