Four points short of a hundred
A routine Lighthouse audit surfaced two accessibility defects on the duel word panel: a missing accessible name and a token the snake could not share.
When the Lighthouse audit ran to confirm the mobile performance improvements, the home page and Daily Challenge both posted clean 100s across accessibility. The AI Duel page, however, stopped at 96. Four points short of a clean sweep was not a reason to halt deployment, but it pointed directly at two overlooked defects on the follow-the-action word panel: an input that screen readers could not name, and a text contrast failure hiding inside the dark theme.
Naming the active input#
The first defect failed aria-input-field-name. When DuelWordPanel was created, the letter container was given role="textbox" along with aria-live="polite" so assistive technologies would announce typed characters as a player entered words. Visually, a small “YOU” label sat directly above the letter cells in a separate container. To a screen reader, however, that visual label was an unrelated sibling element, leaving the input itself completely unlabelled. The fix attached an explicit aria-label whenever the panel enters its input state, while deliberately leaving it off during opponent turns and animations:
<!-- DuelWordPanel.svelte -->
<div
class="word-panel__chars"
class:word-panel__chars--draft={isDraft}
role={isInput ? 'textbox' : undefined}
aria-label={isInput ? strings.duelWordInputAriaLabel : undefined}
aria-readonly={isInput ? 'true' : undefined}
aria-live={isInput ? 'polite' : undefined}
aria-atomic={isInput ? 'true' : undefined}
>
Because the interface is bilingual, the label pulled from the localized configuration dictionary (“Your word” in English and “Ditt ord” in Norwegian). Component tests were updated to assert that the label appears when a player types, and vanishes when the word starts animating across the board.
The token the snake could not share#
The second defect was a subtle color contrast violation. The “YOU” label text in the dark theme used --color-snake-head against --color-surface-raised. In the design system, that combination produced a contrast ratio of 2.87:1, well below the WCAG AA minimum of 4.5:1 for body copy. The obvious temptation was simply adjusting --color-snake-head to increase its lightness, but that variable is not merely an interface accent: it defines the color of the player’s actual snake head rendered on the board canvas. Lightening the token to satisfy text contrast would wash out the snake in the arena:
/* app.css */
/* Same hue and chroma as --color-snake-head, lightness raised: the snake-head
value itself is only 2.87:1 against --color-surface-raised, below AA's
4.5:1 for the word panel's "YOU" label text. Kept as its own token rather
than retuning --color-snake-head, which also colors the on-board snake. */
--color-word-panel-label: oklch(70% 0.193 10.9);
The solution introduced --color-word-panel-label, preserving the exact hue and chroma of the snake head while raising lightness to 70% in the dark theme. In the light and retro themes, where --color-snake-head already exceeded the 4.5:1 threshold (at 5.02:1 and 13.2:1 respectively), the new token simply aliases the snake head value. Separating semantic text usage from game-board entity rendering avoided an unintended visual regression on the playing field.
Back to a clean sweep#
With the accessible name in place and the contrast token separated, a fresh Lighthouse pass brought the AI Duel accessibility score back to 100/100, matching the rest of the application. The full suite of 2,559 tests passed cleanly, confirming that small accessibility corrections do not require sacrificing visual identity when tokens are decoupled early.