Skip to content

fix(theme): raise text contrast to 4.5:1 across the console - #6146

Merged
norman-abramovitz merged 7 commits into
cloudfoundry:developfrom
nabramovitz:norm/fix/a11y-text-contrast
Nov 7, 2026
Merged

norman-abramovitz merged 7 commits into
cloudfoundry:developfrom
nabramovitz:norm/fix/a11y-text-contrast

Conversation

@nabramovitz

Copy link
Copy Markdown
Contributor

Towards #4166. Text in the default theme fell short of the 4.5:1 minimum of WCAG 1.4.3 in several places: placeholders, every table link, primary text and buttons, status-colored text, the About admin badge and some dimmed labels. An axe-core color-contrast sweep of 20 pages in light and dark mode found the sites; the theme tokens explain them.

Changes, one commit each

Commit What Before → after
placeholders --input-placeholder (stylesheet and the branding service, which re-sets it at runtime); .form-control placeholder opacity dropped; stepper skip label 2.5 light / 3.1 dark → 4.8 / 5.7
default blues light primary, secondary, accent, info #3b82f6/#60a5fa → #2563eb in the Tailwind theme, theme.config.ts, the service fallback, the sample configs and the docs table; dark primary/info in theme.config.ts → #60a5fa, matching the stylesheet table links 2.5 → 5.2; text-primary 3.7 → 5.2; active tab 3.3 → 4.6; dark primary text 4.0 → 5.8
--color-on-primary new token for text on the primary color: white in light mode, #0f172a in dark mode; used by .btn-primary, the outline hover, the button toggle, the active step and 12 bg-primary template sites (text-on-primary) white on primary 3.7 light / 2.5 dark → 5.2 / 7.0
status text text-success/warning/danger/info utilities → the existing per-mode --*-text tokens; bg-*/border-* keep the brand values; About admin badge → warning tokens green 2.3, amber 2.2, red 3.8 → 7:1+; badge 2.1 → 6.4
dimmed text quota pages, apply-quota dialog, entity summary title: opacity-60 → text-content-muted ~3 → 4.8
lab leftovers active second-level menu item tint 10 % → 6 % (the darker primary on the 10 % tint measured exactly 4.5); home endpoint filter text-white → text-on-primary 4.5 → 4.8 light / 5.2 dark; 2.5 → 7.0 dark

Operators who configure brand colors are unaffected: the defaults changed, not the configuration path. --color-on-primary is not configurable yet; an operator setting a light primary for dark mode gets dark text on it, which is the common case.

Not in this PR

  • Brand danger/success/warning as button backgrounds with white text (.btn-danger 3.8:1) and the stepper's completed-step check mark on green — separate decision on the status palette.
  • Breadcrumbs at 70 % opacity are 24 px semibold (large text) and pass at 3:1.

Tests

  • make check gate green (729 spec files, 5294 tests).
  • Lab, axe-core 4.10.2 color-contrast over 20 pages (home, lists, Create App, Marketplace, services, endpoints, profile, About, an app's summary/routes/variables/events, a space's summary/apps/services/routes/events, org quota, quota definitions):
Build Light Dark
5.6.0-dev.40 (develop) 68 15
5.6.0-dev.42 (this branch) 0 2, both the placeholder of a disabled dropdown at 60 % opacity — inactive controls are exempt from 1.4.3

The batch sweep also lists some dark-mode rows at 1.4:1 on freshly loaded pages; they are the light→dark switch still in progress at the 2 s mark (the same page re-checked after 6 s reports none, and the live computed colors are #f1f5f9 on #334155).

AI disclosure (RFC-0047): prepared with AI coding assistance; reviewed by the author.

Placeholder text in inputs, textareas and the dropdown trigger was a
71 % gray (#9ca3af) on white, 2.5:1, and 55 % gray on the dark input
background, 3.1:1; WCAG 1.4.3 needs 4.5:1. The token now uses the
theme's muted text color: #64748b in light mode (4.8:1) and #94a3b8 in
dark mode (5.7:1). The branding service re-sets the token at runtime,
so its values change too, and the docs table follows.

The .form-control placeholder used the muted color at 70 % opacity,
which undid the muted color's contrast; it now uses the same token at
full opacity. The stepper's skipped-step label was the same 71 % gray
and now uses the inactive-step text color.

AI disclosure (RFC-0047): prepared with AI coding assistance; reviewed by the author.
The default light-mode primary (#3b82f6) measured 3.7:1 on white, and
the accent (#60a5fa) that colors every table link measured 2.5:1; WCAG
1.4.3 needs 4.5:1 for text. Both, with secondary and info, are now
#2563eb: 5.2:1 on white, 5.0:1 on card headers, 4.6:1 on the active
tab tint, and 5.2:1 under white button text. Operators who configure
their own brand colors are unaffected.

The dark-mode primary in theme.config.ts said #3b82f6 (4.0:1 on the
dark content background) while the stylesheet already used #60a5fa
(5.8:1); the config now matches the stylesheet. Each default lives in
the Tailwind theme, the SCSS dark block, theme.config.ts, the branding
service fallback and the sample config files, and the docs table lists
them; all now agree.

AI disclosure (RFC-0047): prepared with AI coding assistance; reviewed by the author.
Primary buttons, the selected segment of a button toggle, the active
step indicator and the "bg-primary text-white" controls in templates
draw white text on the primary color. In dark mode the primary is the
lighter #60a5fa so that it reads as link text, and white on it is
2.5:1; WCAG 1.4.3 needs 4.5:1. No single blue passes as both text and
a background for white text in dark mode.

A new --color-on-primary carries the text color for primary surfaces:
white in light mode (5.2:1 on #2563eb) and the dark ink #0f172a in
dark mode (7.0:1 on #60a5fa). The button, toggle and stepper styles
use it, and the templates use the matching text-on-primary utility.

AI disclosure (RFC-0047): prepared with AI coding assistance; reviewed by the author.
The text-success, text-warning, text-danger and text-info utilities
used the brand status colors, which are tuned for badges and buttons:
as text on white they measure 2.3:1 (green, the running-app state),
2.2:1 (amber) and 3.8:1 (red); WCAG 1.4.3 needs 4.5:1. The theme
already has per-mode status text tokens (--success-text and friends,
7:1 or better on the content background in both modes), so the four
text utilities now use them. Backgrounds and borders keep the brand
values.

The About page's Admin badge and row used the orange 700 shade on an
orange tint (2.1:1 and 2.5:1); they now use the warning background and
text tokens (6.4:1 light, 6.3:1 dark).

AI disclosure (RFC-0047): prepared with AI coding assistance; reviewed by the author.
The quota and space quota pages, the apply-quota dialog and the entity
summary title drew their labels and subtitles at 60 % opacity, about
3:1 on white; WCAG 1.4.3 needs 4.5:1. They now use the theme's muted
text color, as the faded-text fix did elsewhere. The dialog's load
error used a raw red-600 and now uses text-danger.

AI disclosure (RFC-0047): prepared with AI coding assistance; reviewed by the author.
AI disclosure (RFC-0047): prepared with AI coding assistance; reviewed by the author.
The after-measure on the lab left two cases. The active second-level
menu item drew the primary text on a 10 % primary tint, which with the
darker default primary measures exactly 4.5:1; the tint is now 6 %
(4.8:1 light, 5.2:1 dark). The home page's endpoint filter set white
text on the selected segment directly, missing the on-primary token;
it now uses text-on-primary (2.5:1 → 7.0:1 in dark mode).

AI disclosure (RFC-0047): prepared with AI coding assistance; reviewed by the author.

@norman-abramovitz norman-abramovitz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@norman-abramovitz
norman-abramovitz merged commit c7ba1c9 into cloudfoundry:develop Nov 7, 2026
24 checks passed
@nabramovitz
nabramovitz deleted the norm/fix/a11y-text-contrast branch November 7, 2026 19:25
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