Skip to content

chore: guard the contrast and theming contract in CI - #206

Merged
SishirP17 merged 1 commit into
mainfrom
chore/css-contract-guards
Aug 18, 2026
Merged

chore: guard the contrast and theming contract in CI#206
SishirP17 merged 1 commit into
mainfrom
chore/css-contract-guards

Conversation

@SishirP17

Copy link
Copy Markdown
Collaborator

Nothing in lint, typecheck, or the test suite could see the bug in #202. The stylesheet rule looked correct in isolation; only its interaction with a lower-specificity rule broke it. These are the checks that would have caught it.

scripts/css-contract-scan.ts

Wired into check.sh and the security-scan CI job, following the security-surface-scan.ts pattern. Eight rules, each one a bug that actually shipped:

  1. background-color longhand in a rule that declares no image of its own — the original defect, stated as a decidable rule.
  2. Brand rgba outside :root — the values that cannot follow an agency.
  3. Raw hex outside :root — the old test banned hex in .tsx but exempted the stylesheet, which is how ~27 stray values accumulated there.
  4. var() references with no :root definition.
  5. AGENCY_THEME_VARIABLES against :root, both directions. This is the one that permanently closes the original hole: a token derived from the brand can no longer be added without being made overridable, and a resolver-managed token cannot lack a static default.
  6. --color-brand-raw used as a text color — it deliberately holds the unadjusted agency color and may fail AA.
  7. The hex-alpha-append idiom from fix(web): replace invalid alpha-append tints with color-mix #203.
  8. Dead var() fallbacks.

Verified by reintroducing the original defect:

$ node --import tsx scripts/css-contract-scan.ts
CSS contract scan found 1 issue(s):
  packages/web/src/index.css:377: `.btn-ghost` sets the background-color longhand
  without declaring its own background-image. A longhand cannot clear an inherited
  image, so the fill you asked for may never render. Use the `background` shorthand.
exit code: 1

Against the stylesheet as it stood before this work it reports 130 issues.

design-system.test.ts

Gains a table of real (background, foreground) pairings. It previously only checked white-against-token, so it had no way to know which foreground met which background — which is exactly why .btn-ghost passed it. Translucent tokens are composited before scoring, since the contrast of an 8% tint is not the contrast of the color it is mixed from.

Borders are deliberately not asserted at 3:1, with the reasoning in a comment: WCAG 1.4.11 covers non-text content required to identify a control, and these are decorative separators.

lib/contrast-audit.ts

A dev-only DOM walker for what static rules cannot reach. It resolves each text node's effective background by climbing through transparent ancestors, and when it meets a background-image it parses the gradient's color stops and scores the worst one — precisely the case that made this bug invisible to static analysis.

window.__contrastAudit(), or ?contrast=1 to outline offenders in the page. Confirmed tree-shaken out of the production bundle.

Last of five, stacked on #205.

@SishirP17
SishirP17 requested a review from durga710 as a code owner August 18, 2026 22:42
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rayhealth-evv-platform-app Ready Ready Preview Aug 18, 2026 11:08pm

@SishirP17
SishirP17 force-pushed the fix/contrast-offenders branch from 75ef74a to 4ad1c20 Compare August 18, 2026 23:03
@SishirP17
SishirP17 changed the base branch from fix/contrast-offenders to main August 18, 2026 23:06
Nothing in lint, typecheck, or the test suite could see the button gradient
bug. The stylesheet rule looked correct in isolation; only the interaction
with a lower-specificity rule broke it. These are the checks that would
have caught it.

scripts/css-contract-scan.ts, wired into check.sh and the security-scan CI
job. Eight rules, each one a bug that actually shipped:

  1. background-color longhand in a rule that declares no image of its own,
     which is the original defect stated as a decidable rule.
  2. brand rgba outside :root, the values that cannot follow an agency.
  3. raw hex outside :root. The old test banned hex in .tsx but exempted
     the stylesheet, which is how ~27 stray values accumulated there.
  4. var() references with no :root definition.
  5. AGENCY_THEME_VARIABLES against :root, both directions. This is the one
     that permanently closes the original hole: a token derived from the
     brand can no longer be added without being made overridable, and a
     resolver-managed token cannot lack a static default.
  6. --color-brand-raw used as a text color. It deliberately holds the
     UNADJUSTED agency color and may fail AA.
  7. the hex-alpha-append idiom.
  8. dead var() fallbacks.

Verified by reintroducing the original defect: the scan fails with the
exact line and reason, and passes again on restore. Against the stylesheet
as it stood before this work it reports 130 issues.

design-system.test.ts gains a table of real (background, foreground)
pairings. It previously only checked white-against-token, so it had no way
to know which foreground met which background and .btn-ghost passed it.
Translucent tokens are composited before scoring, since the contrast of an
8% tint is not the contrast of the color it is mixed from.

lib/contrast-audit.ts is a dev-only DOM walker for the cases static rules
cannot reach. It resolves each text node's effective background by climbing
through transparent ancestors, and when it meets a background-image it
parses the gradient stops and scores the worst one , exactly the case that
made this bug invisible. Call window.__contrastAudit(), or append
?contrast=1 to outline offenders. It is tree-shaken out of production.
@SishirP17
SishirP17 force-pushed the chore/css-contract-guards branch from f4603ad to 82934e1 Compare August 18, 2026 23:06
@SishirP17
SishirP17 merged commit cd1cebd into main Aug 18, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant