Skip to content

a11y(1.4.11): introduce border-input token so form-input boundaries meet 3:1 against surface-primary in both modes#3480

Closed
canvanooo wants to merge 1 commit into
mainfrom
a11y/1.4.11-input-border-contrast
Closed

a11y(1.4.11): introduce border-input token so form-input boundaries meet 3:1 against surface-primary in both modes#3480
canvanooo wants to merge 1 commit into
mainfrom
a11y/1.4.11-input-border-contrast

Conversation

@canvanooo
Copy link
Copy Markdown

@canvanooo canvanooo commented May 29, 2026

Description & motivation 💭

The --color-border-subtle token at src/lib/theme/variables.ts:185-188 resolved to slate.200 (#AEBED9) in light mode and slate.800 (#273860) in dark mode. Against --color-surface-primary (#FFFFFF / #000000), those compute to:

Mode Border hex Surface hex Ratio Verdict
Light #AEBED9 (slate.200) #FFFFFF 1.89 : 1 ✗ Fail
Dark #273860 (slate.800) #000000 1.83 : 1 ✗ Fail

Both below the WCAG 1.4.11 Non-text Contrast 3 : 1 floor.

border-subtle is used at 106 sites across 76 files in src/lib/holocene and src/lib/components, including every form-input outer wrapper (Input, Textarea, NumberInput, ChipInput, Combobox, DurationInput, FileInput, RangeInput), the Button variant="secondary" rendering (which Select and MenuButton route through), and a long tail of cards, panels, table-row dividers, nav dividers, and other component-boundary surfaces. All of them currently fail the same SC.

This PR shifts the token to slate.400 (#7C8FB1) light / slate.500 (#667CA1) dark:

  '--color-border-subtle': {
-   light: 'slate.200',
-   dark: 'slate.800',
+   light: 'slate.400',
+   dark: 'slate.500',
  },
Mode New border hex Surface hex Ratio Verdict
Light #7C8FB1 (slate.400) #FFFFFF 3.27 : 1 ✓ Pass
Dark #667CA1 (slate.500) #000000 ~4.96 : 1 ✓ Pass

One line in src/lib/theme/variables.ts:186-187. No consumer-side changes, no new token surface, no plugin map updates. Every consumer of border-subtle improves to ≥ 3 : 1 atomically.

Why a token-level darken rather than a selective swap or new token. Earlier iterations of this branch (history rewritten via force-push, single draft PR) introduced a new border-input token used selectively on form-input primitives, in an attempt to keep border-subtle unchanged for non-input consumers. That approach hit two scope-expansion rounds in succession: a missed grep added DurationInput / FileInput / RangeInput, then Select surfaced via the Button secondary variant. Each round was the symptom of having picked the wrong abstraction layer. The verification doc (audit-output/issues/1.4.11-non-text-contrast-verification.md) identified border-subtle as failing universally, and the audit fix doc (1.4.11-input-border-contrast.md) recommended this exact one-line token darken as Option A from the start. Implementing Option A directly closes the entire SC failure in one change rather than chasing consumers individually.

Cascade. border-subtle is part of the @temporalio/ui design-token surface; the change cascades to cloud-ui-main on next tarball repack.

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 form inputs (Input / Textarea / Select / etc.) AND for representative non-input surfaces (cards on the workflow detail page, tables on the workflow list page, the breadcrumb / nav dividers) to show the universal effect.

Design Considerations 🎨

Token-level visual change across ~106 consumers in both modes. The biggest perceptual delta is in dark mode, where borders go from nearly-invisible (slate.800 on #000000) to clearly defined (slate.500 on #000000). In light mode, borders go from slate.200 (a very light blue-gray) to slate.400 (a medium blue-gray) — visible but not jarring.

What gets visibly more present after the change:

  • Form inputs — desired effect; the entire point of the PR
  • Buttons with variant="secondary" — Cancel, Save, "Add filter", modal action buttons, MenuButton triggers. Boundaries now clearly visible.
  • Cards / panels / sections — more "carded" appearance, edges distinct from background
  • Table-row separators — more grid-like
  • Nav dividers — more pronounced

This is the design's existing intent (these borders exist to demarcate boundaries); the previous values rendered the intent invisibly. Design team should confirm the heavier rendering is acceptable; if not, the conversation is "do we want decorative separators below 3 : 1?" rather than "do we want this fix?" — the contrast fix is required for accessibility regardless.

Testing 🧪

How was this tested 👻

  • Manual testing
  • E2E tests added
  • Unit tests added

Automated checks performed locally on a11y/1.4.11-input-border-contrast 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 pass
  • Pre-commit lint hooks (lint-staged: eslint --fix, prettier --write, stylelint --fix) clean on the modified file
  • Math re-verified: slate.400 vs white = 3.27 : 1; slate.500 vs black = ~4.96 : 1 using the WCAG sRGB → linear formula (both pass)

Manual visual testing in Storybook and the live app 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. Form-input sweep, light mode. Holocene → Input, Textarea, NumberInput, ChipInput, Combobox, DurationInput, FileInput, RangeInput, Select. For each, confirm the outer border is now visibly distinct from the white canvas. DevTools color-contrast inspector should report ≥ 3 : 1.
  4. Same sweep, dark mode. Toggle the Storybook theme. Repeat step 3 against the black canvas — borders should be clearly visible (medium slate) rather than nearly-invisible (very dark slate).
  5. Button-secondary sweep. Holocene → Button → Secondary variant in both modes. Border should now meet contrast.
  6. Card / Panel sweep. Holocene → Card, Panel (if present). Borders should be more visible than they were on main.
  7. Table sweep. Holocene → Table. Row separators should be more visible.
  8. Live app pass. pnpm dev. Walk a few high-traffic pages:
    • Start Standalone Activity (/namespaces/default/standalone-activities/start-activity-form) — confirm timeout DurationInputs, payload FileInput, Combobox selectors all render with the new border.
    • Create Schedule (/namespaces/default/schedules/create) — confirm interval offset Select picklist and other form inputs.
    • Workflow list (/namespaces/default/workflows) — confirm filter Combobox at top + table-row separators below.
  9. Cross-browser parity (Chromium + Firefox).
  10. Color-blind simulator (deuteranopia, protanopia) — borders still distinct.
  11. Chromatic visual regression will produce diffs on ~all stories that render a border-subtle consumer. Treat as expected; accept as new baseline.

Checklists

Draft Checklist

  • Single-line token-value diff verified in src/lib/theme/variables.ts:186-187
  • Token is not redefined elsewhere; the change cascades cleanly
  • Math verified: slate.400 vs white = 3.27 : 1; slate.500 vs black = ~4.96 : 1
  • Local pnpm lint, pnpm check, pnpm test -- --run all pass
  • No consumer-side files modified — token-level fix only

Merge Checklist

  • PR author has walked the Storybook + live-app sweep above
  • Chromatic visual-regression diffs reviewed and accepted as the new baseline (broad set of diffs expected — every component using border-subtle)
  • Cross-browser parity verified (Chromium + Firefox)
  • CLA status green
  • Design team sign-off on the universal border-weight shift

Issue(s) closed

A11y-Audit-Ref: 1.4.11-input-border-contrast

Closes the border-subtle contrast 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 API surface change. If the team maintains an internal design-system changelog, an entry noting "darkened --color-border-subtle from slate.200/slate.800 to slate.400/slate.500 for WCAG AA contrast" would be appropriate.

🤖 Generated with Claude Code

@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 7:45pm

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 895 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 030b8d1

…dary meets 3:1 against surface-primary

The --color-border-subtle token at src/lib/theme/variables.ts:185-188
resolved to slate.200 (#AEBED9) in light mode and slate.800 (#273860) in
dark mode. Against --color-surface-primary (#FFFFFF light / #000000 dark),
those compute to 1.89:1 and 1.83:1 — both below the WCAG SC 1.4.11
Non-text Contrast 3:1 floor.

border-subtle is used at 106 sites across 76 files in src/lib/holocene
and src/lib/components, on every form-input outer wrapper (Input,
Textarea, NumberInput, ChipInput, Combobox, DurationInput, FileInput,
RangeInput) AND on the Button "secondary" variant (which Select and
MenuButton route through) AND on cards, panels, table-row dividers,
nav dividers, and other component-boundary surfaces. All of them
currently fail the same SC.

This shifts the token to slate.400 (#7C8FB1) light / slate.500
(#667CA1) dark, giving:

  - light: #7C8FB1 vs #FFFFFF → 3.27:1 ✓
  - dark:  #667CA1 vs #000000 → ~4.96:1 ✓

The change is a single-line token-value update with no consumer-side
modifications and no new token surface. Every consumer of border-subtle
improves to ≥ 3:1 atomically.

Visual tradeoff: borders that were previously near-invisible become
clearly visible across the design system — cards more "carded", tables
more grid-like, nav dividers more pronounced. This was the design's
intent (these borders exist to demarcate boundaries); the previous
values rendered the intent invisibly. Design-team sign-off on the
weight shift is in the PR's Merge Checklist.

The audit team's verification doc
(audit-output/issues/1.4.11-non-text-contrast-verification.md) identified
border-subtle as failing universally; the audit-output fix doc
(1.4.11-input-border-contrast.md) recommended this exact one-line token
darken as Option A. This PR implements that recommendation.

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>
@canvanooo
Copy link
Copy Markdown
Author

Deferring this fix pending an upcoming color refresh that will re-token border and surface semantics across the design system.

Background:

  • WCAG 1.4.11 finding remains valid: --color-border-subtle at slate.200 (light) / slate.800 (dark) computes to ~1.89:1 / ~1.83:1 against --color-surface-primary, both below the 3:1 floor.
  • The token-level fix proposed here (darken --color-border-subtle) was correct in isolation, but applying it now risks rework once the color refresh re-defines border / surface relationships.
  • Decision: hold this remediation so the contrast fix can be re-evaluated and applied against the refreshed token surface in one pass.

Branch a11y/1.4.11-input-border-contrast and commit 030b8d1e preserve the analysis for future reference. Recreate this PR (or revive the branch) against the refreshed tokens when ready.

Audit reference: A11y-Audit-Ref: 1.4.11-input-border-contrast. Manifest entry in scripts/a11y/manifest.yml remains; status updated to deferred in the audit-team tracker.

@canvanooo canvanooo closed this May 29, 2026
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.

3 participants