Summary
default.css's @theme inline block registers --color-destructive-foreground: var(--destructive-foreground); (line 74), which is what makes the text-destructive-foreground / bg-destructive text-destructive-foreground utility classes exist for Tailwind to generate. But --destructive-foreground itself is never assigned a value anywhere in :root (lines 80-113) or .dark (lines 115-147) — every other token registered in @theme inline (--destructive, --card, --primary, …) has a matching declaration in both blocks; this one does not.
The practical effect: any component author who writes text-destructive-foreground gets a CSS
custom property that resolves to nothing, so the browser falls back to color: inherit (or the
initial value, depending on cascade) rather than a color chosen to be legible against
--destructive. Two call sites in the current tree already do this
(prize-list-editor.tsx:143, sersavan/multi-select.tsx:58), both via bg-destructive text-destructive-foreground.
Control: the sibling token --destructive (also referenced from @theme inline, line 39) is
declared in both :root (line 96) and .dark (line 130) — so this is specifically the
-foreground counterpart that's missing, not a broader problem with the destructive palette.
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 confirming absence with git grep |
How to see it
$ git grep -n "destructive-foreground" upstream/main -- packages/ui/src/styles/default.css
upstream/main:packages/ui/src/styles/default.css:74: --color-destructive-foreground: var(--destructive-foreground);
That is the only hit in the file: line 74, inside @theme inline. Neither :root (lines
80-113) nor .dark (lines 115-147) contains a --destructive-foreground: declaration.
Two live usages that inherit whatever color was cascading in from a parent element instead of
an intentional value:
$ git grep -n "destructive-foreground" upstream/main -- 'apps/*.tsx' 'packages/*.tsx'
upstream/main:apps/builder/src/features/minigames/components/prize-list-editor.tsx:143: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
upstream/main:packages/ui/src/components/ui/sersavan/multi-select.tsx:58: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
Evidence
The measurement script reports the token as missing, not as a numeric contrast failure — that
distinction (missing vs. failing) is itself the evidence:
$ pnpm exec tsx medir.ts default.css
claro [ ... 'claro/estado-fg-destructive falta destructive-foreground' ... ]
oscuro [ ... 'oscuro/estado-fg-destructive falta destructive-foreground' ... ]
By contrast, --destructive itself (the background half of the pair) resolves fine in both
modes and produces no "falta" (missing) row for the criteria that use only it (e.g.
destructivo-boton, error-borde).
Where it comes from
packages/ui/src/styles/default.css (commit 975c7a02a963d80bf140bb269d773b4912b8e7b6):
line 74 (@theme inline registration) vs. the absence of a matching declaration anywhere in
lines 80-147 (:root/.dark).
Why we think this is a defect rather than the intended design
--color-destructive-foreground would not be registered in @theme inline at all if
destructive-foreground weren't meant to be a real, themed color — every other -foreground
counterpart in the same file (--primary-foreground, --secondary-foreground,
--accent-foreground, --card-foreground, --popover-foreground, --sidebar-*-foreground)
has both the @theme inline registration and a :root/.dark value. destructive-foreground
is the only one where the registration exists but the value doesn't — that asymmetry, present in
a file that otherwise pairs every registration with a declaration, reads as an omission rather
than a deliberate "no destructive text color" design choice.
Suggested fix
Values from a3-destructive.css (verified against the criteria above):
:root {
--destructive-foreground: oklch(0.985 0 0); /* white */
}
.dark {
--destructive-foreground: oklch(0.145 0 0); /* near-black */
}
White reads 4.89:1 against --destructive in light mode; the near-black ink reads 6.91:1
against --destructive in dark mode (white would only read 2.87:1 there — the two modes need
different foregrounds, which is exactly why this can't be fixed by hardcoding text-white at
the two call sites instead of fixing the token). Re-running the measurement script against the
patched file removes exactly the two "falta" rows and adds none:
$ pnpm exec tsx medir.ts a3-destructive.css
claro [ ...(same rows as before, minus 'claro/estado-fg-destructive falta destructive-foreground')... ]
oscuro [ ...(same rows as before, minus 'oscuro/estado-fg-destructive falta destructive-foreground')... ]
Workaround for anyone hitting this
The two current call sites (prize-list-editor.tsx:143, sersavan/multi-select.tsx:58) both
already pair bg-destructive with text-destructive-foreground; until the token is declared,
replacing text-destructive-foreground with a hardcoded text-white dark:text-[oklch(0.145_0_0)]
at each call site reproduces the intended contrast, at the cost of duplicating the value instead
of deriving it from the theme. Not verified beyond the two call sites listed above.
What we did not verify
- Whether any other component (outside
apps/**/*.tsx and packages/**/*.tsx, e.g. email
templates) also references text-destructive-foreground.
- The actual rendered
color a browser falls back to in the two live call sites (we established
the custom property resolves to nothing from the CSS source; we did not capture a live DOM
computed-style screenshot of the fallback in a running app).
Related issues — none of these describes it
Searched gh search issues --repo ChatbotXIO/ChatbotX "destructive-foreground" (no --state
filter); zero results. Witness that the mechanism works: the same command with "bug" returns
real issues (e.g. #1226, #1228).
Summary
default.css's@theme inlineblock registers--color-destructive-foreground: var(--destructive-foreground);(line 74), which is what makes thetext-destructive-foreground/bg-destructive text-destructive-foregroundutility classes exist for Tailwind to generate. But--destructive-foregrounditself is never assigned a value anywhere in:root(lines 80-113) or.dark(lines 115-147) — every other token registered in@theme inline(--destructive,--card,--primary, …) has a matching declaration in both blocks; this one does not.The practical effect: any component author who writes
text-destructive-foregroundgets a CSScustom property that resolves to nothing, so the browser falls back to
color: inherit(or theinitial value, depending on cascade) rather than a color chosen to be legible against
--destructive. Two call sites in the current tree already do this(
prize-list-editor.tsx:143,sersavan/multi-select.tsx:58), both viabg-destructive text-destructive-foreground.Control: the sibling token
--destructive(also referenced from@theme inline, line 39) isdeclared in both
:root(line 96) and.dark(line 130) — so this is specifically the-foregroundcounterpart that's missing, not a broader problem with the destructive palette.Environment
975c7a02a963d80bf140bb269d773b4912b8e7b6(upstream/main)git fetch upstream && git rev-parse upstream/main, then readingpackages/ui/src/styles/default.cssat that commit and confirming absence withgit grepHow to see it
That is the only hit in the file: line 74, inside
@theme inline. Neither:root(lines80-113) nor
.dark(lines 115-147) contains a--destructive-foreground:declaration.Two live usages that inherit whatever
colorwas cascading in from a parent element instead ofan intentional value:
Evidence
The measurement script reports the token as missing, not as a numeric contrast failure — that
distinction (missing vs. failing) is itself the evidence:
By contrast,
--destructiveitself (the background half of the pair) resolves fine in bothmodes and produces no "falta" (missing) row for the criteria that use only it (e.g.
destructivo-boton,error-borde).Where it comes from
packages/ui/src/styles/default.css(commit975c7a02a963d80bf140bb269d773b4912b8e7b6):line 74 (
@theme inlineregistration) vs. the absence of a matching declaration anywhere inlines 80-147 (
:root/.dark).Why we think this is a defect rather than the intended design
--color-destructive-foregroundwould not be registered in@theme inlineat all ifdestructive-foregroundweren't meant to be a real, themed color — every other-foregroundcounterpart in the same file (
--primary-foreground,--secondary-foreground,--accent-foreground,--card-foreground,--popover-foreground,--sidebar-*-foreground)has both the
@theme inlineregistration and a:root/.darkvalue.destructive-foregroundis the only one where the registration exists but the value doesn't — that asymmetry, present in
a file that otherwise pairs every registration with a declaration, reads as an omission rather
than a deliberate "no destructive text color" design choice.
Suggested fix
Values from
a3-destructive.css(verified against the criteria above):White reads 4.89:1 against
--destructivein light mode; the near-black ink reads 6.91:1against
--destructivein dark mode (white would only read 2.87:1 there — the two modes needdifferent foregrounds, which is exactly why this can't be fixed by hardcoding
text-whiteatthe two call sites instead of fixing the token). Re-running the measurement script against the
patched file removes exactly the two "falta" rows and adds none:
Workaround for anyone hitting this
The two current call sites (
prize-list-editor.tsx:143,sersavan/multi-select.tsx:58) bothalready pair
bg-destructivewithtext-destructive-foreground; until the token is declared,replacing
text-destructive-foregroundwith a hardcodedtext-white dark:text-[oklch(0.145_0_0)]at each call site reproduces the intended contrast, at the cost of duplicating the value instead
of deriving it from the theme. Not verified beyond the two call sites listed above.
What we did not verify
apps/**/*.tsxandpackages/**/*.tsx, e.g. emailtemplates) also references
text-destructive-foreground.colora browser falls back to in the two live call sites (we establishedthe custom property resolves to nothing from the CSS source; we did not capture a live DOM
computed-style screenshot of the fallback in a running app).
Related issues — none of these describes it
Searched
gh search issues --repo ChatbotXIO/ChatbotX "destructive-foreground"(no--statefilter); zero results. Witness that the mechanism works: the same command with
"bug"returnsreal issues (e.g. #1226, #1228).