Skip to content

A11Y-001: the kitchen display's text is readable on its ambient wash - #1006

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

NewWorldVenture merged 3 commits into
mainfrom
claude/a11y-001-display-ambient-etzqg7

Conversation

@NewWorldVenture

Copy link
Copy Markdown
Owner

A11Y-001: the kitchen display (/display) is the open "/display at narrow widths" item. The defect is not about width or theme.

The defect

The display paints a time-of-day gradient behind every tile. It is on by default (ambient: true in lib/display/ambient.ts). Each theme has bright stops:

  • pink-300 at dawn;
  • sky-300 in the morning;
  • orange-400 in the evening;
  • amber-400 and cyan-400 in others.

Most of the display's text is text-white/50–/60 (50 of its 66 text-white/NN classes were below /70).

axe could not see it. The wash is a fixed -z-10 layer, so axe measured against the page's body colour instead:

  • dark theme: "passes";
  • light theme at 390 px: "1.04:1, white on #f6f7fc", which is not what is on screen either.

Measured from the pixels instead: a seeded local household, Chromium, the evening theme. Every visible text leaf is recorded with its computed colour, all text is then made transparent, and the background is sampled under each text box. The darkest-to-lightest pixel is the worst case. The text colour is composited over it as the browser would.

Text below AA (worst pixel) main This branch
390 px 16 of 17 leaves, 1.4–3.4:1 (e.g. "Bubaly Kitchen" 1.66, the setup card's body 2.10, "Saturday, October 10" 2.98) 3 of 17, all over the featured recipe's photo (see "Not covered")
1280 px 16 of 22 leaves, 1.85–3.71:1 (e.g. "Today's Schedule" 2.70, "Nothing scheduled today" 2.59) 1 of 22, the featured recipe's label over its photo

The fix

  • A scrim. The display's own ink (#0b1020) lies at 75% over the wash and its glows (a new fixed -z-10 layer after them, only when ambient is on). Each theme keeps its hue, at a quarter of the strength.
  • Text opacity. Body text on the display, and on its reconnect and error screens, is text-white/70 or more: 50 classes went from /25–/60, plus the timer input's placeholder. Hover states that were already /70–/100 are unchanged.
  • The floating hints pill ("✅ 20 chores due today — who's on it?") is bg-black/70, not /45, so it holds 4.5:1 even over a white photo pixel.

The non-ambient display (a solid #0b1020) and the photo backdrop mode are otherwise unchanged.

Tests

tests/the-kitchen-display-text-is-readable-on-its-ambient-wash.test.ts reads, from the source:

  • every colour a theme's wash or glow can put behind a tile (lib/display/ambient.ts);
  • the scrim (display-grid.tsx);
  • every bare text-white/NN on the display (components/display/*.tsx, the page and its error screen).

It then composes, the way the browser does:

  • the faintest text, on a bg-white/10 tile (the strongest tint behind body text), over each theme's brightest colour: 4.5:1 at worst required;
  • solid white on the setup card's bg-white/15 button, over the same colours;
  • the hints pill over a white pixel.

Non-vacuity: dawn's and morning's bright stops are found, ambient is on by default, there is a scrim, and there are more than 40 text classes. The control is the same text without the scrim, under 3:1.

Check main This branch
The new test 3 of its 4 source cases fail (the control passes) 5/5
The 39 display suites — 859/859
Pixel measurement above (author-run, local production builds, not committed) 16/17 and 16/22 below AA 3/17 and 1/22, all on the recipe photo
tsc / eslint — 0 / clean

Not covered: text over photographs

The featured recipe tile puts its label ("Featured Recipe", white/70), its name and its category over the recipe photo. The photo's overlay (from-black/80 via-black/25 to-transparent) leaves bright plate pixels behind the label: 1.00 and 2.34 at the worst pixel at 390 and 1280 px. At 390 px the text block covers most of the short tile, so holding 4.5:1 against every pixel would take about 70% black over nearly the whole photo. That is a design decision, so I have left it for you. The options are:

  • a stronger, taller scrim;
  • a solid band behind the text;
  • a smaller title at narrow widths;
  • a text halo.

The photo backdrop mode (background: 'photos') has the same question.

The hints pill's new value is verified by the arithmetic in the test. A pixel reading of it was inconclusive, because the pill fades between hints.

No migration, configuration or dependency change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4


Generated by Claude Code

The kitchen display (/display) paints a time-of-day gradient behind
every tile, on by default. Its brightest stops (pink-300 at dawn,
sky-300 in the morning, orange-400 in the evening, amber and cyan in
others) put the display's text at 1.7 to 3.7:1, measured from the
rendered pixels with the text hidden, on a seeded local household at
390 and 1280 px; most of it was white/50-60. axe could not see it: the
wash is a fixed -z-10 layer, so axe measured against the page's body
colour instead ("passes" in the dark theme, "1.04" in the light one).

- The display's own ink (#0b1020) lies at 75% over the wash and its
  glows, so each theme keeps its hue.
- Body text on the display, and on its reconnect and error screens, is
  white/70 or more (50 classes, from /25-/60), and so is the timer
  input's placeholder.
- The floating hints pill ("20 chores due today — who's on it?") is
  bg-black/70, not /45: over a pale photo at 390 px it read 1.4:1.

tests/the-kitchen-display-text-is-readable-on-its-ambient-wash.test.ts
reads the stops and glows (lib/display/ambient.ts), the scrim
(display-grid.tsx) and every bare text-white/NN on the display, then
composes the faintest text on a white/10 tile, and white on the
white/15 setup button, over each theme's brightest colour: 4.5:1 at
worst. The hints pill clears 4.5:1 over a white pixel. Without the
scrim the same text reads under 3:1 (the control).
On main, 3 of its 4 cases fail.

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 6:02am UTC

Request Review

Copy link
Copy Markdown
Owner Author

Audit follow-up on 06754d5 — bounded repair/test work for the existing owner.

P2: the ambient contrast test omits nested surfaces and hover states. The 10px timer duration text (white/70) is on a white/10 button, white/20 hovered, inside the display-grid white/6 tile. Using the same morning #7dd3fc stop and 75% black scrim, composing all layers predicts about 4.27:1 idle and 3.43:1 hovered. The 14px semibold setup-guide label is white on white/15, white/25 hovered, inside its own white/7 card; it is outside the grid. That predicts about 5.72:1 idle and 4.41:1 hovered. These are source-derived bounds, not observed pixel failures.

Please extend the existing test to derive and compose the actual parent/child layers and interactive states, adjust both controls to clear 4.5:1, and verify the timer picker and setup-guide interactions in the bright ambient themes. Preserve keyboard/focus behavior and existing handlers. The loading text's animate-pulse also changes effective opacity, so do not treat the bare white/70 class as a full-animation guarantee.

Scrim ordering and pointer exclusion look coherent. Photo/recipe contrast remains the separately acknowledged open scope. Return the repair commit and exact-head tests; distinguish arithmetic checks from rendered measurements. No deployment or production changes.

…r them

The audit of #1006 found the ambient-wash test composed each text over one
surface. The timer picker's duration was white/70 on a white/10 button
(white/20 hovered) inside the grid's white/6 tile: 4.27:1 idle and 3.43:1
hovered on the morning stop. The setup guide was white on white/15 (white/25
hovered) in its own white/7 card: 4.41:1 hovered. And a white/70 under
animate-pulse is white/35 at the pulse's faintest frame.

tests/helpers/display-surfaces.ts now reads each text's parents from the JSX
with the TypeScript parser, follows a component up to where it is mounted
(KitchenTimers in WidgetBody in a grid tile; the setup card above the grid),
and composes every surface idle, hovered, focused and at a looping
animation's faintest frame (from Tailwind's own keyframes), keeping cn()
conditions consistent across one rendering. Over every text on the wash,
it found, and this fixes:

- timer preset duration white/70 -> white: 6.73 idle, 5.13 hovered
- "Add timer" white/80 -> white: hovered 3.95 -> 5.13
- setup guide hovered white/25 -> white/20: 4.41 -> 5.01
- a finished timer pulsed the whole card, so its label read 2.89 and
  "Done!" 2.44 at the faintest frame; the pulse is now a layer under the
  words (6.34 and 4.95 at its brightest)
- loading text under animate-pulse white/70 -> white: 3.19 -> 5.31 at the
  faintest frame
- the schedule's "now" row: time emerald-300 -> emerald-200 (4.41 -> 5.25)
  and its Now badge emerald-300 on emerald-400/20 -> emerald-100 on
  emerald-500/20 (3.37 -> 4.90)

Those are arithmetic (alpha compositing, WCAG luminance). The new spec,
tests/e2e/the-kitchen-display-text-is-readable-on-its-ambient-wash.spec.ts,
measures the same controls in Chromium: real DisplayShell, setup card and
timers with the app's compiled stylesheet, the wash painted flat in each of
the nine themes' brightest colour, two screenshots per text (with and
without it). On the morning stop it reads 5.14 for the hovered duration,
5.05 for the hovered guide, 4.97 for "Done!"; on the code before this
commit it read 3.41, 4.44 and 2.43 at the faintest frame. Keyboard and
pointer behaviour (Tab, Enter, Space, the picker's close, Got it's write)
is exercised there unchanged.

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

Copy link
Copy Markdown
Owner Author

Re 01:21Z (nested surfaces and hover states), at d77aae16e, which sits on a merge of current main (cf3b79c0c, no conflicts). Handlers, keyboard and focus are unchanged.

What changed

  • The timer duration and "Add timer" text go to full white. The finished timer's pulse moves to an aria-hidden layer under steady text, so the words no longer fade.
  • The setup guide's hover tint goes from white/25 to white/20. The loading text and the schedule's "now" row and badge, which a new sweep found, are raised.
  • The contrast test now reads the JSX with the TypeScript parser and follows each component to where it is mounted (grid tile white/6, setup card white/7). It composes every text in five states: idle, hovered (on the element and any ancestor), focused, and the faintest frame of a looping animation, taken from Tailwind's own keyframes. A control fed the pre-fix classes reproduces your 4.27/3.43 and 5.72/4.41 exactly.

Arithmetic, worst stop sky-300 under the 0.75 scrim (before → after)

Control Idle Hovered
Timer duration 4.27 → 6.73 3.43 → 5.13
Setup guide 5.72 → 5.72 4.41 → 5.01
Add timer 3.95 → 5.13

The finished label goes from 2.89 to 6.34, loading text from 3.19 to 5.31, the now-row time from 4.41 to 5.25, and the Now badge from 3.37 to 4.90.

Rendered. A new spec drives the real DisplayShell, setup card and KitchenTimers on the compiled CSS, in Chromium at 1280×900, DPR 2, dark. It measures glyph pixels against the same pixels without text, in all nine wash themes. Morning: duration 6.75 idle and 5.14 hovered, guide 5.79 and 5.05 hovered, and every theme clears 4.5. Run on the pre-fix code, it measures 4.25 and 3.41 for the duration and 4.44 for the guide hovered. A behaviour case checks that Tab, Enter and Space start and reopen a timer and that Got it dismisses the card.

Checks: tsc exit 0; eslint --no-ignore on the 7 changed files; npm run i18n:gate clean; 24 related vitest files (330 tests); the new spec 11/11. The new unit cases fail 5 of 11 against the pre-fix components. An independent review found nothing blocking.

Not covered: the page's own radial tint is in the rendered spec only. Rendering is dark theme at one viewport. Text over photos stays out of scope as before.


Generated by Claude Code

@NewWorldVenture
NewWorldVenture marked this pull request as ready for review October 11, 2026 06:50
@NewWorldVenture
NewWorldVenture merged commit c0fee4e into main Oct 11, 2026
9 checks passed
NewWorldVenture pushed a commit that referenced this pull request Oct 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
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