Skip to content

Wave 4 — the accessibility sweep: chart tables, measured contrast, and a survey you can cross by keyboard - #27

Merged
Randolin merged 2 commits into
mainfrom
claude/product-strategy-positioning-u1vwig
Aug 28, 2026
Merged

Randolin merged 2 commits into
mainfrom
claude/product-strategy-positioning-u1vwig

Conversation

@Randolin

Copy link
Copy Markdown
Owner

The fourth wave of docs/feature-plan.md, done as one audit rather than a checklist. Two of the three items turned out differently from what the plan predicted, and those are the interesting parts.

C3 · The numbers behind the charts

Every chart carries role="img" and a one-line summary, which says a shape exists without saying what it says. The radar and the values strips now fold out a real table of the same values — rows are questions, columns are people, cells are the answers themselves rather than the 0..1 the shapes are drawn from. Unanswered renders as a dash, never a zero: "didn't answer" and "answered zero" are different facts, and the strip above draws only one dot.

The seeking matrix deliberately got nothing. A failing test sent me to look properly, and it was already a real table — scoped headers, a visible level label in every cell. A second table saying the same thing is worse for a screen reader than one. A spec now holds that panel to exactly one table, because the obvious future "consistency fix" is to add the disclosure everywhere.

C4 · Contrast, measured rather than eyeballed

Three light-theme values were failing:

token was now why it mattered
--muted 3.4:1 4.9:1 the colour of .fine — the honest-limits copy, the last text here that should be hard to read
--series-4 2.1:1 3.9:1 below even the 3:1 non-text graphics need
--series-3 2.7:1 4.0:1 same — and found by the spec, after I had eyeballed two hues and moved on

The green is the point: the hues that fail are not the ones that look faint, they are the ones whose luminance sits near the page's. Since contrast is measurable it is now tested — contrast.spec.ts reads the token file and checks text at 4.5:1 and graphics at 3:1 across both themes, so the next palette change cannot quietly undo this.

Also answers prefers-contrast: more (greys give up first; series hues stay put, since darkening them toward each other costs the distinguishability they exist for) and forced-colors: active (tokens map onto system keywords, charts opt out via forced-color-adjust because four lines all painted CanvasText are four identical lines, and the focus ring is kept explicitly because nothing else will draw one).

C5 · The survey controls

The plan guessed these needed radio semantics. They don't — every answer control here is deselectable (clicking the chosen option clears the answer, because every question is optional), and an ARIA radio group may not behave that way. Announcing them as radios would have been tidier and would have lied. The aria-pressed toggles stay; navigation was the real defect:

  • 462 tab stops in a fully expanded survey, one per option button — seven for a scale, four per interest item — with 70 needed just to cross "What I value". A toll only keyboard users paid, and no mouse user could see. OptionGroupDirective gives each group the composite-widget treatment: one tab stop, arrows within, Home/End, wrapping. The stop follows the selection, so returning to an answered question lands on the answer.
  • The importance control exposed no state at all. Which tier was selected lived in a highlight class, so a screen reader was told nothing about the control that decides how the entire comparison is weighted. It now says so, and both its rows are named groups.

Verification

Full ladder green: format:check, both typechecks, all four unit suites (23 core files / 5 ui / 8 app / 2 server), build, and e2e.

The e2e matters more than usual here: it drives these exact controls by clicking, and a roving tabindex is an easy way to break mouse interaction without noticing.


Generated by Claude Code

claude added 2 commits August 28, 2026 22:42
Every chart here carries role="img" and a one-line summary, which tells a
screen-reader user that a shape exists and roughly how big it is — not
what it says. The radar and the values strips now fold out a real table
of the same values: rows are the questions, columns are the people,
answers are the answers rather than the 0..1 the shapes are drawn from.
Unanswered reads as a dash, never as a zero, because "did not answer"
and "answered zero" are different facts and the strip above draws only
one dot.

The seeking matrix got nothing, deliberately. It turned out to already
be a real table — scoped headers, a visible level label in every cell —
and a second table saying the same thing is worse for a screen reader
than one. A spec now holds it to exactly one table, since the obvious
future "fix" is to add the disclosure everywhere for consistency.

Contrast: measured, not eyeballed, and three light-theme values were
failing. --muted was 3.4:1 under the .fine text it exists for, which is
the honest-limits copy — the last text in this app that should be hard
to read. --series-4 was 2.1:1 and --series-3 2.7:1, both under the 3:1
that non-text graphics need, so a third or fourth person's line was
effectively invisible on a light background. The green was found by the
spec checking all four hues after I had eyeballed two: the ones that
fail are not the ones that look faint, they are the ones whose luminance
sits near the page's.

Since it is measurable it is now tested — contrast.spec.ts reads the
token file and checks text at 4.5:1 and graphics at 3:1 across both
themes, so the next palette change cannot quietly undo this.

Also answers prefers-contrast: more (greys give up first; series hues
stay put, since darkening them toward each other costs the
distinguishability they exist for) and forced-colors: active (tokens map
onto system keywords, charts opt out via forced-color-adjust because
four lines all painted CanvasText are four identical lines, and the
focus ring is kept explicitly because nothing else will draw one).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaDanzXTm6kVSgbNh1eNrs
The audit expected these controls to need radio semantics. They do not:
every answer control here is deselectable — clicking the chosen option
clears the answer, because every question is optional — and an ARIA
radio group may not behave that way. Announcing them as radios would
have been tidier and would have lied. The aria-pressed toggles stay; the
navigation is what was wrong.

A fully expanded survey was 462 tab stops, one per option button: seven
for a scale, four per interest item, one per choice. Seventy of them to
cross "What I value" alone. That is a toll only keyboard users paid, and
no mouse user could see it. OptionGroupDirective gives each group the
composite-widget treatment — one tab stop, arrows within, Home and End
to the ends, wrapping at the edges — so the survey is one stop per
question instead. The tab stop follows the selection, so returning to an
answered question lands on the answer rather than at the start of the
row.

The importance control was worse: which tier was selected lived entirely
in a highlight class, so a screen reader was told nothing about the
state of a control that decides how the whole comparison is weighted.
It now says so, and both its rows are named groups rather than bare
clusters of buttons.

The e2e is what proves this did not break the mouse: it drives these
exact controls by clicking, and a roving tabindex is an easy way to make
that stop working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaDanzXTm6kVSgbNh1eNrs
@Randolin
Randolin merged commit b69514f into main Aug 28, 2026
2 checks passed
@Randolin
Randolin deleted the claude/product-strategy-positioning-u1vwig branch August 28, 2026 23:04
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