Skip to content

Measure the accessibility rules instead of only writing them down - #3

Draft
aievolutionpl wants to merge 1 commit into
mainfrom
claude/design-taste-onboarding-q6c7re
Draft

aievolutionpl wants to merge 1 commit into
mainfrom
claude/design-taste-onboarding-q6c7re

Conversation

@aievolutionpl

Copy link
Copy Markdown
Owner

Follow-up to #1 and #2, both merged.

v2.0.0 wrote the contrast and focus rules down. Nothing checked them — so this repository was violating its own 🔴 BLOCKERS in 14 of 15 styles. This adds the check, then fixes everything it found.

70 files changed · +590 / −114


The new check

scripts/check_contrast.py measures every style palette against WCAG AA: text pairs at 4.5:1, focus indicators at 3:1, in both themes. Two details decide whether it finds anything at all:

  • Dark palettes resolve light-then-dark. Dark blocks are partial overrides, so reading the dark block alone skips exactly the pairs most likely to fail.
  • Translucent colors are composited before measuring. A ring authored as a 5.2:1 blue at 35% alpha paints at 1.8:1, and only the second number matters.

Waivers exist but require a written reason. There are currently none — everything was fixable.

What it found

🔴 Focus rings were invisible in 14 of 15 styles

Every style shipped a translucent focus color (alpha 0.30–0.45), compositing to 1.2–1.8:1 against its own background — the exact blocker ANTI-SLOP.md defines. All are now solid.

Two were also contradicting their own documented §13:

Style Its own §13 says It shipped Now
05-neo-brutalism "thick black 3px+ outline" pink #FF90E8 at 1.98:1 var(--color-text)
06-brutalist-anti-grid "high-contrast outline" neon #CCFF00 at 1.04:1 var(--color-text)

🔴 14 of 15 example pages had no authored focus style at all

No :focus-visible, no :focus, no outline anywhere. The browser default is not what the repo asks for. Each page now ships one, using a palette color verified at 3:1 against that page's own background (4.1:1–18.5:1).

11 label/fill pairs below 4.5:1

White-on-mid-tone button labels at 1.86–4.48:1. Where the fill is a light tint no light label can pass, so the label went dark; elsewhere the fill was darkened imperceptibly and the label kept. Brand hues are preserved either way — the fill is the brand, the label is chrome.

Glassmorphism's accent was the hex ANTI-SLOP names

--color-accent was #8B5CF6 — the exact value ANTI-SLOP.md:53 calls out as the over-used default violet — and it failed contrast at 4.23:1. Two contradictions, one fix: #7C3AED. Still unmistakably violet, off the flagged default, 5.70:1.

gen_tokens.py absorbed misplaced tokens silently

It treated every :root as the light palette, including one nested inside @media (prefers-reduced-motion: reduce). A token misplaced there looked correct in the generated JSON while applying only to reduced-motion users in a browser. I hit this myself mid-change — my own fix landed in the wrong block and the generator made it look right. At-rule blocks are now excluded from parsing.

CI now enforces both checks

The contrast check runs on every push, and a new job renders all 15 example pages at 1440/768/390/360, failing on mobile overflow and uploading the screenshots as an artifact. screenshot.mjs shipped in #2 but only ever ran by hand — a gate nobody runs is not a gate.

One thing deliberately not checked

--color-border at 3:1. WCAG 1.4.11 requires that of boundaries needed to identify a component, not of every hairline — dividers and subtle card edges are exempt, and nearly every design system uses a ~1.2:1 rule for them. My first draft of the checker did flag them, failed all 15 styles, and would have gotten itself switched off. --shadow-focus carries that requirement instead, and it is checked. Hairlines are reported as advisory under --verbose.

Verification

python3 scripts/check_contrast.py   # 226 token pairs — all pass
python3 scripts/validate.py         # 945 structural checks — all pass
node scripts/screenshot.mjs         # 15 examples × 4 viewports — no blockers

I also opened the renders rather than trusting the exit code: the styles still read as themselves — Glassmorphism is still a vivid gradient field with one frosted moment, Neo-Brutalism still snaps.

Two things I did not act on

Both are judgment calls that belong to you, not to me:

  1. Text over gradient fields isn't measurable by token math. Glassmorphism's hero body copy sits on a purple→pink gradient; the checker compares tokens against --color-bg, so it can't see this. Catching it needs rendered-pixel sampling — worth doing, bigger feature.
  2. The example pages use invented metrics ("90M tracks", "4.9★", "85% to artists") for fictional products. Strictly, ANTI-SLOP.md forbids invented statistics — but these are clearly demo pages for invented brands, which is arguably a different category. Rewriting 15 pages' copy is a call about what example pages are for.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MSG5kjLk3ZvmZy2C4B17kP


Generated by Claude Code

v2.0.0 documented the contrast and focus requirements. Nothing checked them, so
this repository was violating its own 🔴 BLOCKERS in 14 of 15 styles. Adding the
check, then fixing everything it found.

New: scripts/check_contrast.py
Measures every style palette against WCAG AA — text pairs at 4.5:1, focus
indicators at 3:1, both themes. Two details decide whether it finds anything:
- Dark palettes are resolved light-then-dark. Dark blocks are partial overrides,
  so reading the dark block alone skips exactly the pairs most likely to fail.
- Translucent colors are composited before measuring. A ring authored as a 5.2:1
  blue at 35% alpha paints at 1.8:1, and only the second number matters.
Waivers exist but require a written reason; there are currently none.

Fixed: focus rings were invisible in 14 of 15 styles
Every style shipped a translucent focus color (alpha 0.30-0.45), compositing to
1.2-1.8:1 against its own background. All are now solid. Two were also
contradicting their own documented §13: 05 specifies a "thick black outline" and
shipped pink at 1.98:1; 06 specifies a "high-contrast outline" and shipped neon
yellow at 1.04:1. Both now use their text color, as their DNA already said.

Fixed: 14 of 15 example pages had no authored focus style at all
No :focus-visible, no :focus, no outline anywhere. The browser default is not
what the repo asks for. Each page now ships one, using a palette color verified
at 3:1 against that page's own background.

Fixed: 11 label/fill pairs below 4.5:1
White-on-mid-tone button labels at 1.86-4.48:1. Where the fill is a light tint
no light label can pass, so the label went dark; elsewhere the fill was darkened
imperceptibly and the label kept. Brand hues are preserved either way — the fill
is the brand, the label is chrome.

Fixed: Glassmorphism's accent was the hex ANTI-SLOP names
--color-accent was #8B5CF6, the exact value ANTI-SLOP.md calls out as the
over-used default violet, and it failed contrast at 4.23:1. Now #7C3AED: still
unmistakably violet, off the flagged default, 5.70:1.

Fixed: gen_tokens.py absorbed misplaced tokens silently
It treated every :root as the light palette, including one nested inside
@media (prefers-reduced-motion: reduce). A token misplaced there looked correct
in the generated JSON while applying only to reduced-motion users in a browser.
I hit this myself mid-change. At-rule blocks are now excluded from parsing.

CI now enforces both checks
The contrast check runs on every push, and a new job renders all 15 example
pages at 1440/768/390/360, failing on mobile overflow and uploading the
screenshots. The render script shipped in 2.0.0 but only ever ran by hand.

Deliberately not checked: --color-border at 3:1. WCAG 1.4.11 requires that of
boundaries needed to identify a component, not of every hairline; dividers and
subtle card edges are exempt. Holding all 15 styles to it would fail them all
and get the check switched off, which costs more than it buys. --shadow-focus
carries that requirement instead, and it is checked. Reported as advisory under
--verbose.

Verified: 226 token pairs pass, 945 structural checks pass, all 15 examples
render clean at every viewport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSG5kjLk3ZvmZy2C4B17kP
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.

2 participants