Summary
default.css defines --primary once in :root (light) and defines it again in .dark with
the identical OKLCH value (oklch(0.488 0.243 264.376)). Every other themeable token in the
file (--background, --card, --muted-foreground, --border, …) gets a distinct, lighter
value in .dark to preserve contrast against the darker surfaces — --primary is the one
color token that doesn't. Against the actual dark-mode surfaces, that fixed blue computes to
2.60:1 on --card and 2.60:1 as a button border/background contrast case (boton-contra-fondo
criterion, i.e. the same primary tested as a filled control against --background) — both well
under WCAG's 4.5:1 (text) / 3:1 (non-text) floors depending on the usage. text-primary alone
appears in 73 files under apps/**/*.tsx and packages/**/*.tsx (measured below), so this
isn't a single stray usage.
The control that isolates this to --primary specifically: every other dark-mode color that
also changed value between :root and .dark (e.g. --foreground, --muted-foreground,
--accent-foreground) passes its corresponding contrast criterion in dark mode. Only the two
tokens upstream left byte-for-byte identical between modes (--primary here; see draft 03 for
--destructive-foreground, which is missing rather than identical) fail.
Environment
|
|
| Defect type |
Static (code) |
| Commit |
975c7a02a963d80bf140bb269d773b4912b8e7b6 (upstream/main) |
| How measured |
git fetch upstream && git rev-parse upstream/main, then reading packages/ui/src/styles/default.css at that commit and running the WCAG 2.x contrast math against its .dark values |
How to see it
packages/ui/src/styles/default.css, commit above:
:root line 88: --primary: oklch(0.488 0.243 264.376);
.dark line 122: --primary: oklch(0.488 0.243 264.376); — byte-identical to line 88
- Compute the WCAG 2.x contrast of
--primary (as text, text-primary) against --card
(oklch(0.21 0.006 285.885) in .dark) and against --background
(oklab(18.288% 0.00014 -0.01275) in .dark).
Evidence
$ pnpm exec tsx medir.ts default.css
...
oscuro [
'oscuro/enlace 2.60',
'oscuro/boton-contra-fondo 2.60',
'oscuro/anillo 1.45',
'oscuro/lateral 3.39',
...
]
oscuro/enlace is text-primary (used as a link/accent text color) against --card;
oscuro/boton-contra-fondo is the same --primary value tested as a filled control's boundary
color against --background. Both read 2.60 — under 4.5:1 and under 3:1 respectively.
Usage count, text-primary as a standalone class (not text-primary-foreground), on
upstream/main at the commit above:
$ git grep -n -E "text-primary([^-]|$)" upstream/main -- 'apps/*.tsx' 'packages/*.tsx' | wc -l
73
(Witness: git grep -c -E "text-primary([^-]|$)" upstream/main -- packages/ui/src/components/ui/button.tsx → 1.)
Where it comes from
packages/ui/src/styles/default.css (commit 975c7a02a963d80bf140bb269d773b4912b8e7b6),
:root line 88 and .dark line 122.
Why we think this is a defect rather than the intended design
Every other surface-dependent color pair in the same file (--foreground/.dark --foreground,
--muted-foreground/.dark --muted-foreground, --accent-foreground/.dark --accent-foreground, etc.) is deliberately re-derived for the darker background — that is the
whole point of a .dark block existing at all. --primary sits alone as the one token that was
seemingly copy-pasted rather than re-derived. We found no comment or test asserting that the
brand's primary blue is meant to look identical, and therefore proportionally lower-contrast, in
dark mode.
Suggested fix
Values from a2-primario.css (verified against the criteria above):
.dark {
--primary: oklch(0.707 0.165 254.624); /* Tailwind blue-400 */
--primary-foreground: oklch(0.21 0.006 285.885);
}
oklch(0.707 0.165 254.624) reads 6.79:1 against --card and 7.17:1 against --background in
dark mode — comfortably above both floors. Re-running the measurement script against the
patched file removes exactly the two affected rows and adds none:
$ pnpm exec tsx medir.ts a2-primario.css
oscuro [
'oscuro/anillo 1.45',
'oscuro/lateral 3.39',
...
]
(oscuro/enlace and oscuro/boton-contra-fondo are gone; no oscuro/boton-texto row appears,
confirming the new value doesn't break the reverse case — primary-foreground-on-primary as a
filled button's text.)
What we did not verify
- Whether
blue-400 specifically is the right hue for this project's brand — any sufficiently
light, sufficiently chromatic blue in the same hue family would clear the floor; we picked the
nearest standard Tailwind stop for a concrete, buildable suggestion.
- Any place outside
apps/**/*.tsx and packages/**/*.tsx (e.g. email templates, marketing
pages) that might also reference --primary and were not counted.
- The APCA/WCAG 3 numbers for this pair — only WCAG 2.x is measured here.
Related issues — none of these describes it
Searched gh search issues --repo ChatbotXIO/ChatbotX "<term>" for dark mode primary and
primary contrast dark (no --state filter); both returned zero results. Witness that the
mechanism works: the same command with "bug" returns real issues (e.g. #1226, #1228).
Summary
default.cssdefines--primaryonce in:root(light) and defines it again in.darkwiththe identical OKLCH value (
oklch(0.488 0.243 264.376)). Every other themeable token in thefile (
--background,--card,--muted-foreground,--border, …) gets a distinct, lightervalue in
.darkto preserve contrast against the darker surfaces —--primaryis the onecolor token that doesn't. Against the actual dark-mode surfaces, that fixed blue computes to
2.60:1 on
--cardand 2.60:1 as a button border/background contrast case (boton-contra-fondocriterion, i.e. the same primary tested as a filled control against
--background) — both wellunder WCAG's 4.5:1 (text) / 3:1 (non-text) floors depending on the usage.
text-primaryaloneappears in 73 files under
apps/**/*.tsxandpackages/**/*.tsx(measured below), so thisisn't a single stray usage.
The control that isolates this to
--primaryspecifically: every other dark-mode color thatalso changed value between
:rootand.dark(e.g.--foreground,--muted-foreground,--accent-foreground) passes its corresponding contrast criterion in dark mode. Only the twotokens upstream left byte-for-byte identical between modes (
--primaryhere; see draft 03 for--destructive-foreground, which is missing rather than identical) fail.Environment
975c7a02a963d80bf140bb269d773b4912b8e7b6(upstream/main)git fetch upstream && git rev-parse upstream/main, then readingpackages/ui/src/styles/default.cssat that commit and running the WCAG 2.x contrast math against its.darkvaluesHow to see it
packages/ui/src/styles/default.css, commit above::rootline 88:--primary: oklch(0.488 0.243 264.376);.darkline 122:--primary: oklch(0.488 0.243 264.376);— byte-identical to line 88--primary(as text,text-primary) against--card(
oklch(0.21 0.006 285.885)in.dark) and against--background(
oklab(18.288% 0.00014 -0.01275)in.dark).Evidence
oscuro/enlaceistext-primary(used as a link/accent text color) against--card;oscuro/boton-contra-fondois the same--primaryvalue tested as a filled control's boundarycolor against
--background. Both read 2.60 — under 4.5:1 and under 3:1 respectively.Usage count,
text-primaryas a standalone class (nottext-primary-foreground), onupstream/mainat the commit above:(Witness:
git grep -c -E "text-primary([^-]|$)" upstream/main -- packages/ui/src/components/ui/button.tsx→1.)Where it comes from
packages/ui/src/styles/default.css(commit975c7a02a963d80bf140bb269d773b4912b8e7b6),:rootline 88 and.darkline 122.Why we think this is a defect rather than the intended design
Every other surface-dependent color pair in the same file (
--foreground/.dark --foreground,--muted-foreground/.dark --muted-foreground,--accent-foreground/.dark --accent-foreground, etc.) is deliberately re-derived for the darker background — that is thewhole point of a
.darkblock existing at all.--primarysits alone as the one token that wasseemingly copy-pasted rather than re-derived. We found no comment or test asserting that the
brand's primary blue is meant to look identical, and therefore proportionally lower-contrast, in
dark mode.
Suggested fix
Values from
a2-primario.css(verified against the criteria above):oklch(0.707 0.165 254.624)reads 6.79:1 against--cardand 7.17:1 against--backgroundindark mode — comfortably above both floors. Re-running the measurement script against the
patched file removes exactly the two affected rows and adds none:
(
oscuro/enlaceandoscuro/boton-contra-fondoare gone; nooscuro/boton-textorow appears,confirming the new value doesn't break the reverse case — primary-foreground-on-primary as a
filled button's text.)
What we did not verify
blue-400specifically is the right hue for this project's brand — any sufficientlylight, sufficiently chromatic blue in the same hue family would clear the floor; we picked the
nearest standard Tailwind stop for a concrete, buildable suggestion.
apps/**/*.tsxandpackages/**/*.tsx(e.g. email templates, marketingpages) that might also reference
--primaryand were not counted.Related issues — none of these describes it
Searched
gh search issues --repo ChatbotXIO/ChatbotX "<term>"fordark mode primaryandprimary contrast dark(no--statefilter); both returned zero results. Witness that themechanism works: the same command with
"bug"returns real issues (e.g. #1226, #1228).