Skip to content

a11y(1.4.11): Checkbox — add ring-offset so the checked-state focus ring contrasts against the indigo checked background#3477

Open
canvanooo wants to merge 1 commit into
mainfrom
a11y/1.4.11-checkbox-checked-focus-ring
Open

a11y(1.4.11): Checkbox — add ring-offset so the checked-state focus ring contrasts against the indigo checked background#3477
canvanooo wants to merge 1 commit into
mainfrom
a11y/1.4.11-checkbox-checked-focus-ring

Conversation

@canvanooo
Copy link
Copy Markdown

@canvanooo canvanooo commented May 29, 2026

Description & motivation 💭

The Holocene Checkbox primitive renders its :checked state with an indigo background (peer-checked:bg-interactive--color-interactive-surface = indigo.600 #444CE7 in both modes per src/lib/theme/variables.ts:124-127) and applies a peer-focus-visible:ring-2 peer-focus-visible:ring-primary/70 focus ring. ring-primary maps to --color-border-focus-info = indigo.600 in both modes (src/lib/theme/plugin.ts:221, variables.ts:213-216).

When a checkbox is checked and focused, Tailwind composites a 2 px ring whose color is indigo.600 at 70% alpha directly on top of an indigo.600 background. Effective ring color equals background color, contrast ratio ~1.00 : 1, focus indicator invisible. Keyboard users tabbing through a form lose the focus indicator the moment a checkbox is checked.

The unchecked-state ring is unaffected (it composites over surface-primary, not over indigo). This PR fixes the checked-state-only failure by adding a 2 px ring-offset between the indigo background and the indigo ring, so the ring composites against the surrounding canvas (surface-primary) rather than against itself.

The diff:

  'peer-focus-visible:ring-2',
  'peer-focus-visible:ring-primary/70',
+ 'peer-focus-visible:ring-offset-2',
+ 'peer-focus-visible:ring-offset-[var(--color-surface-primary)]',

Two lines in src/lib/holocene/checkbox.svelte, inside the !disabled class array of the visual-surrogate <span>.

Design-system finding to surface to reviewers. The arbitrary-value form ring-offset-[var(--color-surface-primary)] is used here rather than the bare class ring-offset-surface-primary. The latter is a silent no-op in the current Tailwind config: src/lib/theme/plugin.ts extends theme.ringColor with primary/danger/success/brand but does NOT extend theme.ringOffsetColor, and the base theme.colors palette has no surface-primary key. I verified empirically with pnpm exec tailwindcss against both class strings — ring-offset-surface-primary generates zero CSS rules (falls back to default #fff); ring-offset-[var(--color-surface-primary)] generates --tw-ring-offset-color: var(--color-surface-primary) correctly. The previously-merged Button-primary focus-ring fix (#3438) uses the bare-class form — it works in light mode by coincidence (default #fff matches light-mode surface-primary) and is visible-but-mis-colored in dark mode. This PR uses the arbitrary-value form so dark mode is also correct. A separate follow-up may want to migrate Button to the same form; that's out of scope here.

Composes with PR #3478. PR #3478 (in flight) shifts the dark-mode value of --color-border-focus-info from indigo.600 to indigo.400 so ring-primary/70 composites at 3.78:1 against dark canvas. That fix addresses what color the ring is. This PR addresses where the ring sits (offset). Together they bring the dark-mode focused-and-checked checkbox to ~8.43:1 ring-vs-canvas contrast. They're independent and can land in either order.

Screenshots (if applicable) 📸

Screenshots to be captured by the PR author from the Vercel preview build (link appears once the Vercel check passes). Include light-mode and dark-mode captures for each affected primitive.

Design Considerations 🎨

The rendered checkbox now has slightly more visual weight when focused-and-checked (4 px total of ring + gap vs the previous 2 px ring overlapping the block). For unchecked + focused, no visual change. Design team may want to confirm the focused-and-checked rendering is acceptable; it's necessary to meet WCAG 1.4.11 because the underlying indigo-on-indigo overlap is what makes the ring invisible.

Testing 🧪

How was this tested 👻

  • Manual testing
  • E2E tests added
  • Unit tests added

Automated checks performed locally on a11y/1.4.11-checkbox-checked-focus-ring before pushing:

  • pnpm lint — 0 errors
  • pnpm check (svelte-check) — 0 errors (84 pre-existing warnings repo-wide, none in checkbox.svelte)
  • pnpm test -- --run — 142 test files / 2023 tests pass
  • Pre-commit lint hooks (lint-staged: eslint --fix, prettier --write, stylelint --fix) clean on the modified file
  • Tailwind class-generation probe (pnpm exec tailwindcss) confirms ring-offset-[var(--color-surface-primary)] emits real CSS (--tw-ring-offset-color: var(--color-surface-primary))

Manual visual testing in Storybook is the responsibility of the PR author after the preview deploy is ready (see "Steps for others to test" below).

Steps for others to test: 🚶🏽‍♂️🚶🏽‍♀️

  1. Check out the branch and pnpm install if needed.
  2. pnpm stories:dev — open Storybook at http://localhost:6006.
  3. Sidebar → Holocene → Checkbox → Default story.
  4. Click the checkbox to toggle it checked. Click outside, then Tab back onto it.
  5. Confirm: visible ring around the checked checkbox, separated from the indigo block by a small gap (indigo block → white gap → indigo ring → page surface).
  6. DevTools eyedropper: ring color materially different from the indigo block color; gap visible.
  7. Toggle Storybook to dark mode (theme toolbar). Repeat steps 4-6. Confirm: gap is black (not white), ring visible. Note: in dark mode the ring contrast still depends on PR a11y(1.4.11): focus rings — lighten dark-mode --color-border-focus-info to indigo.400 so ring-primary/70 meets 3:1 against surface-primary #3478 landing too — see the "Composes with PR a11y(1.4.11): focus rings — lighten dark-mode --color-border-focus-info to indigo.400 so ring-primary/70 meets 3:1 against surface-primary #3478" note above.
  8. Hover (don't focus) over the checkbox. Confirm: hover ring renders unchanged (the new offset only applies to focus-visible).
  9. Try a disabled checkbox. Confirm: no focus ring fires (the new lines are inside the !disabled array).
  10. Try an unchecked focused checkbox. Confirm: rendering identical to current main — the offset is a visual no-op here because the checkbox background already equals surface-primary.
  11. Cross-browser: confirm in Chromium and Firefox.

Checklists

Draft Checklist

  • Two-line diff verified in src/lib/holocene/checkbox.svelte (inside the !disabled class array)
  • Tailwind class generation confirmed via pnpm exec tailwindcss probe
  • Design-system finding (no-op ring-offset-surface-primary in current config) surfaced in PR description
  • Local pnpm lint, pnpm check, pnpm test -- --run all pass

Merge Checklist

  • PR author has walked the Storybook sweep above
  • Light-mode and dark-mode focused-checked rendering both verified
  • Cross-browser parity (Chromium + Firefox)
  • CLA status green
  • Design sign-off on the rendered focused-checked appearance (visible gap between indigo block and indigo ring)

Issue(s) closed

A11y-Audit-Ref: 1.4.11-checkbox-checked-focus-ring

Closes the checkbox checked-state focus-ring defect documented in the May 2026 audit (manifest bucket 1, severity serious, scope ui-main). See scripts/a11y/manifest.yml for the canonical entry.

Docs

Any docs updates needed?

No external docs (docs.temporal.io) need updating — this is a consumer-side class addition with no API surface change.

🤖 Generated with Claude Code

…ing contrasts against the indigo checked background

The Holocene Checkbox primitive's :checked state applied
peer-checked:bg-interactive (--color-interactive-surface = indigo.600 in
both modes) with a peer-focus-visible:ring-primary/70 focus ring, where
ring-primary maps to --color-border-focus-info (also indigo.600 in both
modes). The composited ring color equalled the checked background color
(ratio 1.00:1), so the focus indicator was invisible whenever a checkbox
was both checked and focused — the most common state for tab-traversing a
form.

This adds peer-focus-visible:ring-offset-2 + ring-offset-[var(--color-
surface-primary)] on the visual surrogate. The 2 px gap sits between the
indigo block and the indigo ring, and the gap is colored to match the
surrounding page surface (white in light mode, black in dark mode) so the
ring composites against canvas rather than against itself. Post-fix ratio
against the gap is ~3.58:1 in light mode (Pass).

Note: the existing Button-primary fix in #3438 used the class
ring-offset-surface-primary, which is a silent no-op in the current
Tailwind config (no surface-primary key in theme.ringOffsetColor or
theme.colors). It worked there only because Tailwind's default
--tw-ring-offset-color (#fff) happens to match light-mode surface-primary
and is visible-but-mis-colored in dark mode. The arbitrary-value form
used here, ring-offset-[var(--color-surface-primary)], passes the design
token through verbatim and generates a real CSS rule that is correct in
both modes. A separate follow-up may want to apply the same arbitrary-
value treatment to Button.

Cross-walks 2.4.7 Focus Visible (Level AA). Cascades to cloud-ui-main via
the @temporalio/ui tarball on next repack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

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

Project Deployment Actions Updated (UTC)
holocene Ready Ready Preview, Comment May 29, 2026 4:13pm

Request Review

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions github-actions Bot added a11y Accessibility audit PR a11y:bucket-1 Bucket 1: design-mergeable, CSS / tokens a11y:sc-1.4.11 labels May 29, 2026
@temporal-cicd
Copy link
Copy Markdown
Contributor

temporal-cicd Bot commented May 29, 2026

Warnings
⚠️

📊 Strict Mode: 3 errors in 1 file (0.3% of 898 total)

src/lib/holocene/checkbox.svelte (3)
  • L33:13: Type 'undefined' is not assignable to type 'T'.
  • L34:13: Type 'undefined' is not assignable to type 'T[]'.
  • L13:12: Argument of type '$$Props' is not assignable to parameter of type '{ id?: string | undefined; checked?: boolean | undefined; label?: string | undefined; labelHidden?: boolean | undefined; indeterminate?: boolean | undefined; disabled?: boolean | undefined; ... 5 more ...; class?: string | undefined; }'.

Generated by 🚫 dangerJS against b7a5d16

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a11y:bucket-1 Bucket 1: design-mergeable, CSS / tokens a11y:sc-1.4.11 a11y Accessibility audit PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants