Skip to content

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

Merged
bilal-karim merged 1 commit into
mainfrom
a11y/1.4.11-focus-ring-dark-mode-opacity
Jun 3, 2026
Merged

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
bilal-karim merged 1 commit into
mainfrom
a11y/1.4.11-focus-ring-dark-mode-opacity

Conversation

@canvanooo

@canvanooo canvanooo commented May 29, 2026

Copy link
Copy Markdown
Contributor

Description & motivation 💭

The Tailwind class pattern focus-visible:ring-2 focus-visible:ring-primary/70 is used on 34 sites across 25 files in src/lib/holocene/ and src/lib/components/ — Button (secondary, ghost), Link, Tab, Input, Textarea, Combobox, Checkbox, RadioInput, FileInput, ToggleSwitch, Pill, navigation primitives, and more. ring-primary resolves to --color-border-focus-info, which was set to indigo.600 (#444CE7) in both light and dark modes.

At 70% alpha composited over the dark-mode surface (--color-surface-primary = #141414), the ring renders as approximately #273074. Contrast ratio against the surrounding canvas: ~1.92:1 — below the WCAG 1.4.11 Non-text Contrast floor of 3:1. Light mode passes at 3.58:1; the failure is dark-mode only, but it affects every focus-visible ring rendered there.

This PR shifts the dark-mode value of --color-border-focus-info from indigo.600 to indigo.400 (#8098F9). At 70% alpha composited over the dark surface that resolves to ~#5A6BAF, giving ~3.78:1 against the surrounding canvas. Light mode is byte-identical (still indigo.600).

The diff:

  '--color-border-focus-info': {
    light: 'indigo.600',
-   dark: 'indigo.600',
+   dark: 'indigo.400',
  },

One line in src/lib/theme/variables.ts:215.

Why a token-level change rather than dropping /70 from every consumer. The audit fix doc offered two viable options. Option A: find-replace ring-primary/70ring-primary across 34 sites. Simpler conceptually, but touches 25 files in the design-system layer, changes light-mode ring intensity on every consumer (3.58 → 5.27), and the resulting dark-mode contrast is 3.12:1 — sub-pixel rendering variance away from 3:1. Option B (this PR): change one CSS variable's dark-mode value. Light-mode rendering pixel-identical. Dark-mode contrast 3.78:1 with margin. Token blast radius is minimal — --color-border-focus-info has exactly two references in src/: its own definition in variables.ts:213 and the ringColor.primary mapping in theme/plugin.ts:221. Nothing else (no outline, no border, no text) consumes it. The asymmetric light/dark token pattern is also consistent with neighboring tokens — --color-border-danger is already red.500 light / red.400 dark in this same file.

Composes with PR #3477. That PR adds ring-offset to the Checkbox :checked state to fix the indigo-on-indigo problem (addresses where the ring sits). This PR changes the dark-mode token color (addresses what color it is). 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 🎨

Token-color shift in one mode only. The dark-mode focus ring becomes a lighter indigo (slightly cooler / less saturated). Design team may want to confirm the perceptual change is acceptable — it's necessary for WCAG AA contrast at the current /70 opacity. If the team prefers to keep indigo.600 everywhere, the alternative is Option A (drop /70 opacity), with the tradeoffs documented in the rationale above.

Testing 🧪

How was this tested 👻

  • Manual testing
  • E2E tests added
  • Unit tests added

Automated checks performed locally on a11y/1.4.11-focus-ring-dark-mode-opacity before pushing:

  • pnpm lint — 0 errors
  • pnpm check (svelte-check) — 0 errors (84 pre-existing warnings repo-wide, none introduced by this change)
  • pnpm test -- --run — 142 test files / 2023 tests passed
  • Pre-commit lint hooks (lint-staged: eslint --fix, prettier --write, stylelint --fix) clean
  • Token reference grep confirms --color-border-focus-info is consumed by exactly one Tailwind utility (ring-primary) — no other Tailwind utility, no raw CSS reference

No new automated tests added because the change is a single CSS-variable value; existing Chromatic visual-regression coverage exercises every primitive's focused state in both modes and will produce expected dark-mode-only diffs to accept as new baseline. 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. Toggle the theme switcher in the Storybook toolbar to dark mode.
  4. Tab through these stories and confirm the focus ring is clearly visible against the dark canvas (a lighter indigo, not the previous near-black-on-near-black render):
    • Holocene → Button → Secondary variant
    • Holocene → Button → Ghost variant
    • Holocene → Link → Default
    • Holocene → Tab → Default
    • Holocene → Input → Default (click into the input)
    • Holocene → Textarea → Default
    • Holocene → Combobox → Default
    • Holocene → Checkbox → Default (Tab to it without checking)
    • Holocene → RadioInput → Default
  5. Toggle back to light mode. Tab through the same stories. Confirm the focus rings render identically to current main (no visible change in light mode).
  6. (Optional) Use the DevTools color-contrast inspector on a focused control in dark mode — the rendered ring color against #141414 should report ≥ 3:1.
  7. (Optional) Color-blind simulator (deuteranopia, protanopia, achromatopsia): focus state still distinct from canvas.
  8. Cross-browser: confirm the dark-mode improvement in both Chromium and Firefox.

Checklists

Draft Checklist

  • Token-level diff verified to be 1 line in src/lib/theme/variables.ts
  • --color-border-focus-info consumer audit: 2 references confirmed, both internal to the theme layer
  • Local pnpm lint, pnpm check, pnpm test -- --run all pass

Merge Checklist

  • PR author has walked the dark-mode Storybook sweep above
  • Chromatic visual-regression diffs reviewed and accepted as new baseline (expected: dark-mode focus-ring snapshots only; light-mode snapshots should match existing baselines)
  • CLA status green
  • Design team sign-off on the dark-mode token shift (indigo.600indigo.400 in dark mode only)

Issue(s) closed

A11y-Audit-Ref: 1.4.11-focus-ring-dark-mode-opacity

Closes the focus-ring dark-mode opacity 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 design-system internal token-value change with no surface change in light mode and a subtle color shift in dark mode that doesn't introduce new APIs or behaviors. If the team maintains an internal design-system change-log, an entry noting "dark-mode focus ring color shifted from indigo.600 to indigo.400 for WCAG AA contrast" would be appropriate.

🤖 Generated with Claude Code

…fo to indigo.400 so ring-primary/70 meets 3:1 against surface-primary

The ring-primary/70 pattern (focus-visible:ring-2 focus-visible:ring-primary/70)
is used on 34 sites across 25 files in src/lib/holocene/ and src/lib/components/
— Button, Link, Tab, Input, Textarea, Combobox, Checkbox, RadioInput, FileInput,
ToggleSwitch, Pill, navigation primitives, and more. ring-primary maps to
--color-border-focus-info, which was indigo.600 (#444CE7) in both light and dark
modes.

At 70% alpha composited over the dark-mode surface (--color-surface-primary =
#141414), the ring renders as approximately #273074. Contrast ratio against the
surrounding canvas: ~1.92:1 — below the SC 1.4.11 Non-text Contrast floor of
3:1. Light mode passes at 3.58:1; the failure is dark-mode only.

This shifts the dark-mode value of --color-border-focus-info to indigo.400
(#8098F9), which at 70% over the dark surface composites to ~#5A6BAF, giving
~3.78:1 against the surrounding canvas. Light mode is byte-identical (still
indigo.600).

The change is scoped to one CSS variable assignment because the token flows
exclusively through Tailwind's ring-primary utility (verified: only two
references in src/ — the variable definition itself and the ringColor.primary
mapping in plugin.ts). The fix is intentionally token-level rather than
class-level (Option A in the fix doc) for blast radius and design-judgment
reasons: Option A would touch 34 consumer sites and change light-mode
rendering too. The asymmetric light/dark token pattern is consistent with
neighboring tokens like --color-border-danger (red.500 light / red.400 dark).

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) <[email protected]>
@vercel

vercel Bot commented May 29, 2026

Copy link
Copy Markdown

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:31pm

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

temporal-cicd Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

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

src/lib/theme/variables.ts (3)
  • L223:16: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ readonly '--color-text-black': { readonly light: "space-black"; readonly dark: "space-black"; }; readonly '--color-text-white': { readonly light: "off-white"; readonly dark: "off-white"; }; ... 49 more ...; readonly '--color-border-focus-danger': { ...; }; }'.
  • L224:2: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<--${string}, ${number} ${number} ${number}>'.
  • L225:2: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<--${string}, ${number} ${number} ${number}>'.

Generated by 🚫 dangerJS against 5963eec

@bilal-karim
bilal-karim merged commit 634b08e into main Jun 3, 2026
21 of 22 checks passed
@bilal-karim
bilal-karim deleted the a11y/1.4.11-focus-ring-dark-mode-opacity branch June 3, 2026 20:01
rossedfort added a commit that referenced this pull request Jun 4, 2026
Auto-generated version bump from 2.50.0 to 2.51.0

Bump type: minor

Changes included:
- [`495e27f7`](495e27f) Use pageSize instead of maximumPageSize (#3422)
- [`42c58c45`](42c58c4) fix: use rem-based width for expanded side-nav (#3426)
- [`ce928ed5`](ce928ed) fix(a11y): meet 4.5:1 contrast for text-subtle and text-warning (WCAG 1.4.3) (#3437)
- [`e8ed8a49`](e8ed8a4) fix(a11y): consistent high-contrast focus rings across all Button variants (WCAG 1.4.11) (#3438)
- [`03918fb5`](03918fb) fix(a11y): prevent horizontal scroll on login page at 320px (WCAG 1.4.10) (#3440)
- [`a11555fb`](a11555f) fix(a11y): use rem-based font-size and unitless line-height in markdown reset (#3430)
- [`057ff323`](057ff32) fix: use empty alt on decorative SDK logo image (#3424)
- [`e435ffd7`](e435ffd) Standalone Activity details page UI updates (#3427)
- [`e09028f3`](e09028f) Make bottom-nav accept linksSnippet instead of sections (#3445)
- [`a22a1e21`](a22a1e2) Add tooltip for SDK version (#3462)
- [`c3b7820e`](c3b7820) [DT-4039] Workflow query builder doesn't work with numeric search attributes (#3435)
- [`5e53881f`](5e53881) Make Common Errors dismissable (#3471)
- [`2665e2f1`](2665e2f) [DT-4048] Add accessibility PR triage and notification helpers (#3465)
- [`8083cf4e`](8083cf4) feat(DT-4017): Schedules List UI Update (#3467)
- [`b5de30e3`](b5de30e) fix(markdown): allow nested lists to render as block (DT-4047) (#3463)
- [`8a6fd2de`](8a6fd2d) Add a prop to hide select/deselect controls (#3474)
- [`33ec1b3e`](33ec1b3) DT-4051: pre-populate input when starting standalone activity like this one (#3469)
- [`0d64fc20`](0d64fc2) fix(a11y): reveal Copyable's CopyButton on focus-within (WCAG 2.1.1) (#3452)
- [`321651c0`](321651c) fix: accept autocomplete prop on NumberInput, ChipInput, and Combobox (#3425)
- [`6567c222`](6567c22) fix: cap ZoomSvg container height to viewport (#3428)
- [`afd3a8ed`](afd3a8e) fix(a11y): accommodate text-spacing overrides on Badge and Chip (WCAG 1.4.12) (#3433)
- [`d264b876`](d264b87) fix(a11y): non-color signal for Label required indicator (WCAG 1.4.1) (#3439)
- [`73c61ea3`](73c61ea) fix(a11y): tabindex on <main> so the skip link moves focus reliably (WCAG 2.4.1) (#3451)
- [`34e582a8`](34e582a) fix(a11y): info-and-relationships compliance (WCAG 1.3.1) (#3432)
- [`f7be7b6c`](f7be7b6) fix(query): quote ExecutionDuration Go duration strings in visibility SQL (#3482)
- [`9f0c2631`](9f0c263) Passthrough goto params to link component (#3483)
- [`2f8c037b`](2f8c037) feat: add centerButton, menuButton, and linksContent snippets to BottomNavigation (#3485)
- [`7f258bac`](7f258ba) fix: add "for" to validate connection modal title (#3488)
- [`350397ab`](350397a) feat(DT-4069): Update modal backdrop to 50% opaque (#3486)
- [`f01baa49`](f01baa4) refactor: Input & DatePicker - Svelte 5 & afterLabel snippet (#3479)
- [`587c892f`](587c892) Fix decoded object payload summaries (#3491)
- [`2b85ef06`](2b85ef0) fix(DT-4044): only use browser codec endpoint when override option is selected (#3490)
- [`468893ba`](468893b) fix(a11y): improve 1.1.1 non-text content compliance (#3431)
- [`3e8c996d`](3e8c996) fix(a11y): make Tooltip keyboard-accessible, dismissible, and hoverable (#3429)
- [`47552538`](4755253) fix(a11y): name-role-value compliance, partial (WCAG 4.1.2) (#3434)
- [`e2c95d54`](e2c95d5) fix(a11y): align DOM order with visual order in mobile bottom-nav (WCAG 1.3.2) (#3441)
- [`634b08e1`](634b08e) 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)
- [`039a555a`](039a555) a11y(1.4.11): Checkbox — add ring-offset so the checked-state focus ring contrasts against the indigo checked background (#3477)
- [`01161e2e`](01161e2) a11y(1.4.10): activity-options drawer — make width responsive so it reflows at 320 px (#3476)
- [`99f0a0a4`](99f0a0a) fix(a11y): filter hidden nav items in mobile bottom-nav (WCAG 4.1.2) (#3494)
- [`d12ebacc`](d12ebac) fix(a11y): add focusin/focusout to saved-query nav hover tooltip (WCAG 2.1.1) (#3453)
- [`c7206af9`](c7206af) fix(a11y): render a warning icon for Chip warning intent (WCAG 1.4.1) (#3450)
- [`b64ed713`](b64ed71) fix(a11y): pair Toast variant background with a severity icon (WCAG 1.4.1) (#3449)
- [`8e2ef708`](8e2ef70) upgrade temporal api version to latest (v1.62.13) (#3502)

Co-authored-by: rossedfort <[email protected]>
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.

5 participants