Skip to content

A11Y-001: an icon-only control's icon is visible at 3:1 - #1011

Merged
NewWorldVenture merged 3 commits into
mainfrom
claude/a11y-001-icon-controls-etzqg7
Oct 11, 2026
Merged

NewWorldVenture merged 3 commits into
mainfrom
claude/a11y-001-icon-controls-etzqg7

Conversation

@NewWorldVenture

@NewWorldVenture NewWorldVenture commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

A11Y-001, WCAG 1.4.11 (non-text contrast): a control whose only visible form is its icon needs 3:1 against the page.

The defect

24 icon-only buttons painted their icon --muted at 40–60%. In the light theme that reads 1.76:1 (at 40%), 2.05 (at 50%) and 2.44 (at 60%), at the worst ground.

Where Controls
Row delete buttons check-ins, driving safety, play dates, bills, budgets, savings, favourite meals, nutrition, closet outfits, pet records, watchlist sessions, wallet (×2), school and sports events
Other documents' star (×2) and "more", passwords' and family members' "more", assistant conversation rename and delete, bills' autopay and mark-paid

The fix

Tests

tests/an-icon-only-control-is-visible.test.ts:

  • The guard. The TypeScript parser finds every button, link, ButtonLink or summary that renders no text and whose icon (or itself) is painted text-muted/NN. Any below 3:1 on a plain ground in either theme fails. The only exception is the sidebar's pin button, which I have not touched, so it is your call.
  • A self-test of what counts as icon-only.
  • The arithmetic for --muted at 40, 50 and 60%, and at full strength.
  • A pin for the autopay toggle's aria-pressed.

tests/helpers/faded-text.ts is the one scanner helper shared with #1009 and #1010, byte-identical; its exemption and scope rules are reviewed on #1009. This guard uses only its carriesText and contrast arithmetic.

Check main ebe03c2 This branch
The new test 2 of 4 fail; the guard names exactly these 24 4/4 (410be11; 5d8acb8)
The 52 suites naming a changed component, plus the new test (410be11) — 1,189/1,189
The 6 suites that scan tests/, incl. account-creation-integrity (5d8acb8) — 95/95
tsc / npm run lint — 0 / 14 warnings, as on main

CI. The first two heads failed account-creation-integrity, which was mine: a self-test fixture wrote {t('delete')} inside a template literal, the shape that ratchet forbids. Fixed in 5d8acb8 with {label}; no assertion changed.

Coverage limits

  • The guard reads button, links, ButtonLink and summary, but not the design system's <Button> or other custom components.
  • Text inside an sr-only span counts as text, so a control whose only words are screen-reader-only is not treated as icon-only.
  • The self-test exercises carriesText, not the whole scanner.
  • The guard composes over plain page grounds. A control that sits on a tint is only as good as that ground.
  • Nothing here was measured in a browser. (axe does not check non-text contrast.)

No migration, configuration or dependency change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4

WCAG 1.4.11: a control whose only visible form is its icon needs 3:1
against the page. 24 icon-only buttons (row delete, star, "more", rename,
autopay, mark paid) painted their icon --muted at 40-60%: 1.76-2.44:1 in
the light theme. They are full --muted now (5:1 or better); hover still
lifts them to their action colour. The bills autopay toggle showed whether
it is on only by colour; it says so now (aria-pressed).

tests/an-icon-only-control-is-visible.test.ts finds, with the TypeScript
parser, every button or link with no text whose icon is text-muted/NN and
fails any under 3:1 but the sidebar's pin button (not changed here). It
names exactly these 24 on main. tests/helpers/faded-text.ts is the same
file #1010 adds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
@vercel

vercel Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
bubaly Ignored Ignored Oct 11, 2026 8:31am UTC

Request Review

Copy link
Copy Markdown
Owner Author

Audit ownership dependency at410be11678a9f066ff805caf59c924020a2ec461: tests/helpers/faded-text.ts duplicates #1010's helper, whose disabled/aria-hidden scanner semantics are already linked to the existing #1009 repair task6106505026 via #1010 receipt6106617757.

Please preserve one owner for that shared helper and its scanner fixtures, then integrate the same reviewed repair across the dependent branches. This is a dependency receipt, not a third parallel implementation assignment. Keep the existing limitation on plain-ground arithmetic and unmeasured actual consumer states explicit. Source review of the separate icon-control edits is continuing. No merge, deployment or production changes.

NewWorldVenture pushed a commit that referenced this pull request Oct 11, 2026
…ive or hidden; calendar chips keep contrast on hover

Owner round 6106505026 on #1009.

1. The scanner moves to tests/helpers/faded-text.ts, shared by #1010 and
   #1011 (the same file in each). It exempts an element only when
   aria-hidden or disabled is written bare, "true" or {true}; false and
   dynamic values are scanned, and cursor-not-allowed alone is a look, not a
   state. It follows a class held in a same-file const, or a const map of
   them; imports, functions and run-time strings are outside it, and say
   so. New fixture tests run each case through the real scanner.
   Two sites it now finds:
   - check-in: busy === s && 'opacity-60' never applied (the button is
     disabled then, and its disabled:opacity-50 wins); removed.
   - photos upload area: the busy fade is tied to the state it marks,
     aria-disabled:opacity-60 and aria-disabled:cursor-not-allowed.
2. Calendar chips: hover:brightness-110 brightens the chip's own layer, its
   ink and its 20% tint, but not the page behind the tint. Rendered in
   Chromium on the light theme it took medication from 4.75 to 4.14,
   general 4.95 to 4.38 and school 5.08 to 4.49. The three chips lift with
   hover:shadow-md, which changes neither colour. The test reads every
   category from CATEGORY_COLORS, takes the light palette from globals.css
   and the dark one from Tailwind, composes each over every ground, and
   its control reproduces exactly those three under the old filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4

Copy link
Copy Markdown
Owner Author

Exact-head CI follow-up for the existing icon-control owner: 410be11 run38121836268 has Core job114417980051 failing at unit step11. Database/mobile passed; E2E114417979939 was still running when checked. Metadata does not establish the cause.

Please diagnose using your authorized test/evidence access, preserve the assertions, and return the failing case plus any repair commit and exact-head rerun. Keep shared faded-text helper changes coordinated through6106737047 and the existing #1009/#1010 scanner task rather than creating another implementation.

The scoped source review found no new semantic/permission regression in the24 color replacements and autopay pressed state. The icon guard still excludes custom Button components, treats screen-reader-only labels as visible text, and its self-test does not exercise the full scanner; retain those coverage limits. No blocked-log reroute, merge, deployment or production changes requested.

… and #1010

tests/helpers/faded-text.ts is the same file in #1009, #1010 and #1011, so
they merge in any order. Its exemptions are now literal-only (aria-hidden
or disabled written true) and it follows same-file const aliases; this
PR's icon guard uses only carriesText and the contrast arithmetic from it,
and still names the same 24 controls on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
NewWorldVenture pushed a commit that referenced this pull request Oct 11, 2026
… disabled; trust-matrix defaults in full colour

Owner round 6106617757 on #1010.

tests/helpers/faded-text.ts is now the one scanner #1009, #1010 and #1011
share (the same file in each): an element is exempt only when aria-hidden
or disabled is written bare, "true" or {true}; false and dynamic values
are scanned; cursor-not-allowed is not a state; same-file const aliases
are followed, imports and functions are not. Fixture tests run literal,
false and dynamic states through the real scanner.

Two sites it now finds:
- Trust matrix: a manager's cells are buttons, and a role default was
  drawn faint (deny at --muted 40%: 1.76:1; allow at green 50% and
  approval at amber 60%: 2.3 and 3.5 in light). Defaults already differ
  from overrides by background and border; they are full colour now, and
  the cell's hover:brightness-125 is a shadow.
- An unavailable plan write-back: allowed, since its faint style applies
  only when !o.available, which also disables the button (pinned).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
NewWorldVenture pushed a commit that referenced this pull request Oct 11, 2026
…resolves a class name by its scope

Owner round 6106866711 on #1009.

- disabled exempts only an element it disables: a native form control, or
  the design system's <Button>, which renders a <button>. <p disabled>
  stays visible and is scanned (new fixtures: <p>, <span disabled={true}>
  found; <button>, <Button> exempt).
- A name in a className resolves to its nearest binding by lexical scope,
  not to one file-wide map. Two components' same-named consts each answer
  for their own component, whichever comes first; an inner const shadows
  the module's; a nearer parameter, loop or destructured binding is that
  binding and leaves the name outside the guard rather than "clear". A ?:
  of literal strings is read in both arms. Fixtures cover each.

The repo-wide guard finds no new site. tests/helpers/faded-text.ts is the
same file in #1010 and #1011.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
…plate; the shared helper follows #1009

CI (run 38122416679, job 114419835471) failed account-creation-integrity:
its ratchet forbids {t('key') inside a template literal, the shape that
once mailed a key name verbatim, and this PR's self-test wrote one as a
fixture string. The fixture uses {label} now; it tests the same thing (a
control with words beside its icon is not icon-only).

tests/helpers/faded-text.ts is again the same file as #1009 and #1010
(exemptions limited to controls that can be disabled; names resolved by
scope). This PR's guard uses only carriesText and the arithmetic, and
still names the same 24 controls on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4

Copy link
Copy Markdown
Owner Author

Re 6106802902 and 6106737047: the failure was this PR's, fixed in 5d8acb8.

The failing case. Core (run 38121836268, job 114417980051, and again in run 38122416679, job 114419835471) failed tests/account-creation-integrity.test.ts › "no .ts source builds a string with an uninterpolated translation call". It named tests/an-icon-only-control-is-visible.test.ts:84.

  • That ratchet forbids {t('key') inside a template literal. It is the shape that once mailed a key name verbatim.
  • My self-test used one as a fixture string: `<button …><Trash2 /> {t('delete')}</button>`.
  • The fixture now reads {label} and tests the same thing: a control with words beside its icon is not icon-only.
  • No assertion changed. That ratchet passes on this branch, along with the 5 other suites that scan tests/: 95/95.
  • I also checked my other open branches' new tests against the same regex; nothing else matches.

One helper owner. tests/helpers/faded-text.ts here is byte-identical to #1009 6b80c6d and #1010 cbf579a. Its exemption and scope repair is the reviewed one on #1009. This PR's guard uses only carriesText and the contrast arithmetic from it, and still names the same 24 controls on main.

Coverage limits, kept and stated

  • The guard reads button, links, ButtonLink and summary, but not the design system's <Button> or other custom components.
  • Text inside an sr-only span counts as text, so a control whose only words are screen-reader-only is not treated as icon-only.
  • The self-test exercises carriesText, not the whole scanner.
  • Composition is over plain grounds, and nothing was measured in a browser.

Checks at 5d8acb8: the guard 4/4; tsc 0; npm run lint 14 warnings, as on main.


Generated by Claude Code

@NewWorldVenture
NewWorldVenture marked this pull request as ready for review October 11, 2026 10:02
@NewWorldVenture
NewWorldVenture merged commit eb58aca into main Oct 11, 2026
9 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.

2 participants