Problem
Three separate things make the destructive surface look wrong, most visibly on the alert-dialog's "Delete account" button.
1. The red is Tailwind red-600 verbatim (oklch(0.577 0.245 27.325)). Chroma 0.245 sits near the sRGB gamut edge, so it vibrates against white and reads hot and generic next to this project's warm editorial palette.
2. Dark mode makes the fill translucent. button.ts carries dark:bg-destructive/60, so the confirm button for a destructive action renders at 60% opacity over a near-black surface. Measured: oklab(0.704 0.176821 0.072217 / 0.6). It reads dusty and closer to disabled than to dangerous, which is the wrong signal for the one button that should look most certain. Attenuation is already this kit's disabled vocabulary (disabled:opacity-50).
3. The dark --destructive-foreground token is broken. Light is oklch(0.97 0.01 17) (near-white); dark is oklch(0.58 0.22 27), a mid-dark RED. Anything painting text-destructive-foreground on a bg-destructive surface renders red on red in dark mode. Nothing hits it today only because button.ts hardcodes text-white, so this is a latent trap rather than a live bug, and it will bite the first component that uses the token pair as intended.
Design / approach
Deepen the red slightly, drop the dark translucency, fix the foreground token.
| token |
light |
dark |
--destructive |
oklch(0.52 0.20 25) |
oklch(0.62 0.17 25) |
--destructive-foreground |
oklch(0.97 0.01 17) (unchanged) |
oklch(0.97 0.01 17) |
Chroma drops 0.245 to 0.20 in light, which keeps unmistakable danger semantics while stopping the vibration against white. Dark gets a lighter, slightly less saturated red so it holds up at FULL opacity against a near-black background. Hue moves 27.3 to 25, a marginal shift away from orange so it does not blur into the brand accent.
Removing dark:bg-destructive/60 from the button's destructive variant is what lets the dark token do its job. This diverges from shadcn, which ships that opacity; the divergence is presentation only, so the API parity that matters (variant and size names, data attributes) is untouched, consistent with the accordion underline and hover divergences before it.
Candidates were rendered side by side in both themes before choosing; the alternatives were a warm brick (oklch(0.50 0.17 30), rejected as reading like a warning rather than a delete) and a cooler crimson (oklch(0.54 0.21 15), rejected as fighting the warm palette).
Implementation notes (for the implementing agent)
Where to edit:
packages/ui/packages/registry/themes/index.css: --destructive and --destructive-foreground in the :root block (~L82-83) and in the dark block (~L117-118).
packages/ui/packages/registry/components/button.ts: the destructive entry in the variant map (~L47-48). Remove dark:bg-destructive/60. Leave text-white, hover:bg-destructive/90, and the focus-ring classes alone.
packages/ui/packages/registry/themes/base-colors.js: check whether any per-base-colour override redeclares --destructive; if so it needs the same treatment or it will diverge per theme.
website/public/input.css: the gallery preview scope declares its own copy of the shadcn palette (.ui-preview, the prefers-color-scheme block, and the :root[data-theme='dark'] .ui-preview block). All three destructive declarations must be updated in step or the gallery previews will disagree with what webjs ui init writes into a user's app.
Landmines / gotchas:
- The website copy is NOT generated from the registry theme; it is a hand-maintained mirror in
input.css. Updating only the registry leaves the gallery showing the old red. This is the single most likely way to half-land this change.
- After editing, run
node website/scripts/copy-registry.mjs (component sources) and npm run css:build --workspace=@webjsdev/website, then HARD-restart the dev server: its watcher has been observed serving stale output, and a stale render is exactly how a colour change looks like it did nothing.
- Verify in a real browser in BOTH themes at
/ui/alert-dialog and /ui/button (the destructive variant appears in the button's Variants pane too).
theme-* items are synthesized on demand by the composer from themes/index.css plus base-colors.js, so a theme edit changes what webjs ui init --base-color X writes for every colour.
Invariants to respect:
- shadcn API parity is names and data attributes, not pixel-identical presentation (packages/ui/AGENTS.md invariant 5).
- AGENTS.md invariant 11 for any prose added to comments.
Tests + docs surfaces:
node --test packages/ui/test/registry-contents.test.js (asserts hallmark class strings; check whether it pins dark:bg-destructive/60).
node --test packages/ui/test/base-colors.test.js if the base-colour overrides are touched.
- Website suites (
npm test --prefix website, npm run test:browser --workspace=@webjsdev/website).
- No markdown surface documents the destructive colour values; the
Design tokens used line in button.ts already names --destructive, so it stays accurate.
Acceptance criteria
Problem
Three separate things make the destructive surface look wrong, most visibly on the alert-dialog's "Delete account" button.
1. The red is Tailwind
red-600verbatim (oklch(0.577 0.245 27.325)). Chroma 0.245 sits near the sRGB gamut edge, so it vibrates against white and reads hot and generic next to this project's warm editorial palette.2. Dark mode makes the fill translucent.
button.tscarriesdark:bg-destructive/60, so the confirm button for a destructive action renders at 60% opacity over a near-black surface. Measured:oklab(0.704 0.176821 0.072217 / 0.6). It reads dusty and closer to disabled than to dangerous, which is the wrong signal for the one button that should look most certain. Attenuation is already this kit's disabled vocabulary (disabled:opacity-50).3. The dark
--destructive-foregroundtoken is broken. Light isoklch(0.97 0.01 17)(near-white); dark isoklch(0.58 0.22 27), a mid-dark RED. Anything paintingtext-destructive-foregroundon abg-destructivesurface renders red on red in dark mode. Nothing hits it today only becausebutton.tshardcodestext-white, so this is a latent trap rather than a live bug, and it will bite the first component that uses the token pair as intended.Design / approach
Deepen the red slightly, drop the dark translucency, fix the foreground token.
--destructiveoklch(0.52 0.20 25)oklch(0.62 0.17 25)--destructive-foregroundoklch(0.97 0.01 17)(unchanged)oklch(0.97 0.01 17)Chroma drops 0.245 to 0.20 in light, which keeps unmistakable danger semantics while stopping the vibration against white. Dark gets a lighter, slightly less saturated red so it holds up at FULL opacity against a near-black background. Hue moves 27.3 to 25, a marginal shift away from orange so it does not blur into the brand accent.
Removing
dark:bg-destructive/60from the button's destructive variant is what lets the dark token do its job. This diverges from shadcn, which ships that opacity; the divergence is presentation only, so the API parity that matters (variant and size names, data attributes) is untouched, consistent with the accordion underline and hover divergences before it.Candidates were rendered side by side in both themes before choosing; the alternatives were a warm brick (
oklch(0.50 0.17 30), rejected as reading like a warning rather than a delete) and a cooler crimson (oklch(0.54 0.21 15), rejected as fighting the warm palette).Implementation notes (for the implementing agent)
Where to edit:
packages/ui/packages/registry/themes/index.css:--destructiveand--destructive-foregroundin the:rootblock (~L82-83) and in the dark block (~L117-118).packages/ui/packages/registry/components/button.ts: thedestructiveentry in the variant map (~L47-48). Removedark:bg-destructive/60. Leavetext-white,hover:bg-destructive/90, and the focus-ring classes alone.packages/ui/packages/registry/themes/base-colors.js: check whether any per-base-colour override redeclares--destructive; if so it needs the same treatment or it will diverge per theme.website/public/input.css: the gallery preview scope declares its own copy of the shadcn palette (.ui-preview, theprefers-color-schemeblock, and the:root[data-theme='dark'] .ui-previewblock). All three destructive declarations must be updated in step or the gallery previews will disagree with whatwebjs ui initwrites into a user's app.Landmines / gotchas:
input.css. Updating only the registry leaves the gallery showing the old red. This is the single most likely way to half-land this change.node website/scripts/copy-registry.mjs(component sources) andnpm run css:build --workspace=@webjsdev/website, then HARD-restart the dev server: its watcher has been observed serving stale output, and a stale render is exactly how a colour change looks like it did nothing./ui/alert-dialogand/ui/button(the destructive variant appears in the button's Variants pane too).theme-*items are synthesized on demand by the composer fromthemes/index.cssplusbase-colors.js, so a theme edit changes whatwebjs ui init --base-color Xwrites for every colour.Invariants to respect:
Tests + docs surfaces:
node --test packages/ui/test/registry-contents.test.js(asserts hallmark class strings; check whether it pinsdark:bg-destructive/60).node --test packages/ui/test/base-colors.test.jsif the base-colour overrides are touched.npm test --prefix website,npm run test:browser --workspace=@webjsdev/website).Design tokens usedline inbutton.tsalready names--destructive, so it stays accurate.Acceptance criteria
/60--destructive-foregroundis near-white, so atext-destructive-foregroundonbg-destructiveis legible in dark/ui/alert-dialogand/ui/button, after a hard dev-server restart