From 83bccc0ff10d62f25182399451e45f4d10290424 Mon Sep 17 00:00:00 2001 From: Vivek Date: Mon, 27 Jul 2026 23:49:31 +0530 Subject: [PATCH 1/8] fix(ui): deepen the destructive red and stop dark mode fading it The destructive surface was Tailwind red-600 verbatim (chroma 0.245, near the sRGB gamut edge), which vibrates against white and reads generic next to this project's warm palette. It now sits at chroma 0.20 in light and a lighter 0.17 in dark, which keeps unmistakable danger semantics without shouting. Dark mode also faded the fill to 60% opacity, compositing to a dusty red that read closer to disabled than dangerous, on the one control that should look most certain. Attenuation is already this kit's disabled vocabulary, so the dark variant is gone and dark uses the full token, which is tuned lighter for dark backgrounds. Fixes a latent trap alongside: dark --destructive-foreground was a mid red where light is near-white, so any component painting text-destructive-foreground on bg-destructive would have rendered red on red. Nothing hits it today only because the button hardcodes text-white. Applied to both the registry theme and the gallery's preview scope, which is a hand-maintained mirror rather than a generated one. --- packages/ui/packages/registry/components/button.ts | 10 +++++++++- packages/ui/packages/registry/themes/index.css | 6 +++--- website/public/input.css | 10 +++++----- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/ui/packages/registry/components/button.ts b/packages/ui/packages/registry/components/button.ts index 52ce9bee9..e7c4d8ad5 100644 --- a/packages/ui/packages/registry/components/button.ts +++ b/packages/ui/packages/registry/components/button.ts @@ -44,8 +44,16 @@ const BASE = const VARIANTS = { default: 'bg-primary text-primary-foreground hover:bg-primary/90', + // No dark:bg-destructive/60 here, deliberately. shadcn fades the destructive + // fill to 60% in dark, which composites to a dusty, washed-out red against a + // near-black surface: it reads closer to disabled than to dangerous, and + // attenuation is already this kit's DISABLED vocabulary (disabled:opacity-50). + // The confirm button for a destructive action is the one control that should + // look most certain, so dark uses the full token, which is tuned lighter for + // dark backgrounds. Presentation-only divergence; the variant and size names + // and the data attributes match shadcn exactly. destructive: - 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40', + 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40', outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', diff --git a/packages/ui/packages/registry/themes/index.css b/packages/ui/packages/registry/themes/index.css index e368f6f48..b66e3ed7a 100644 --- a/packages/ui/packages/registry/themes/index.css +++ b/packages/ui/packages/registry/themes/index.css @@ -79,7 +79,7 @@ --muted-foreground: oklch(0.556 0 0); --accent: oklch(0.97 0 0); --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); + --destructive: oklch(0.52 0.20 25); --destructive-foreground: oklch(0.97 0.01 17); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); @@ -114,8 +114,8 @@ --muted-foreground: oklch(0.708 0 0); --accent: oklch(0.371 0 0); --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(0.58 0.22 27); + --destructive: oklch(0.62 0.17 25); + --destructive-foreground: oklch(0.97 0.01 17); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); diff --git a/website/public/input.css b/website/public/input.css index a517ef58e..868241ef4 100644 --- a/website/public/input.css +++ b/website/public/input.css @@ -137,7 +137,7 @@ --muted-foreground: oklch(0.556 0 0); --accent: oklch(0.97 0 0); --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); + --destructive: oklch(0.52 0.20 25); --destructive-foreground: oklch(0.97 0.01 17); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); @@ -160,8 +160,8 @@ --muted-foreground: oklch(0.708 0 0); --accent: oklch(0.371 0 0); --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(0.58 0.22 27); + --destructive: oklch(0.62 0.17 25); + --destructive-foreground: oklch(0.97 0.01 17); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); @@ -183,8 +183,8 @@ --muted-foreground: oklch(0.708 0 0); --accent: oklch(0.371 0 0); --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(0.58 0.22 27); + --destructive: oklch(0.62 0.17 25); + --destructive-foreground: oklch(0.97 0.01 17); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); From 7945b33839dcdf6b0776e7a4b04344218f9c6587 Mon Sep 17 00:00:00 2001 From: Vivek Date: Tue, 28 Jul 2026 00:17:51 +0530 Subject: [PATCH 2/8] fix: make the destructive red legible in dark mode The destructive button rendered a faded, dusty red on a near-black surface, reading closer to disabled than to dangerous on the one control that should look most certain. --destructive plays two roles that pull its lightness in opposite directions: a FILL behind --destructive-foreground, and TEXT on a card. In dark mode the text role needs it light, which leaves it too light for white text on the fill. shadcn resolves that with dark:bg-destructive/60, fading the fill until white clears AA, so the opacity was quietly doing contrast work rather than muting a colour. Pair the fill with its own foreground token instead. Dark now runs a full-strength light red with DARK text, light keeps a deeper red with near-white text, and both roles are legible with no compositing trick. The dark --destructive-foreground was also unusable before this (a mid red, so anything honouring the token pair painted red on red); nothing hit it only because button.ts hardcoded text-white. Chroma drops from Tailwind red-600's 0.245, which sits near the sRGB gamut edge and vibrates against white, and the hue shifts marginally off orange so it does not blur into the brand accent. The new test converts the real tokens through OKLab to sRGB and asserts the WCAG ratio for every pairing a shipped component paints, across all seven base colours. A bad hand-edited oklch() triple looks fine in a screenshot and fails only for the people who most need it not to. --- .../ui/packages/registry/components/badge.ts | 9 +- .../ui/packages/registry/components/button.ts | 30 ++- .../ui/packages/registry/themes/index.css | 6 +- packages/ui/test/destructive-contrast.test.js | 179 ++++++++++++++++++ website/public/input.css | 10 +- 5 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 packages/ui/test/destructive-contrast.test.js diff --git a/packages/ui/packages/registry/components/badge.ts b/packages/ui/packages/registry/components/badge.ts index 837fd4ce5..383f11e84 100644 --- a/packages/ui/packages/registry/components/badge.ts +++ b/packages/ui/packages/registry/components/badge.ts @@ -13,7 +13,8 @@ * (not focusable, no tabindex). Only an interactive badge (an * or