Skip to content

fix(ui): deepen the destructive red and stop dark mode fading it #1138

Description

@vivek7405

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

  • The destructive button is a full-opacity red in dark mode, no /60
  • Light and dark reds match the table above, in BOTH the registry theme and the gallery's preview scope
  • Dark --destructive-foreground is near-white, so a text-destructive-foreground on bg-destructive is legible in dark
  • Verified in a real browser in both themes, on /ui/alert-dialog and /ui/button, after a hard dev-server restart
  • Kit and website suites green

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions