You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Neither light override redeclares it (:root[data-theme='light'] at L187, nor the @media (prefers-color-scheme: light) block at L210), but both DO set --card: oklch(1 0 0). So light mode paints a red chosen for a near-black surface onto a white card.
alertClass({ variant: 'destructive' }) is bg-card text-destructive … (examples/blog/components/ui/alert.ts:37), which puts that red text on the white card at 2.91:1, against the 4.5:1 minimum. It renders on real user paths:
examples/blog/modules/auth/components/auth-forms.ts:106, the failed-login message
examples/blog/modules/posts/components/new-post.ts:76, the failed-post-submit message
An error message is the worst possible place for low contrast, and a failed login is exactly when a user is already having trouble. Dark mode is fine (6.88:1); the bug is light-mode only, which is the usual way this escapes notice.
Near-white text on that light red is 2.59:1. Nothing renders it today (nothing in the app uses a filled destructive variant, and the app's owned components/ui/button.ts:40 still carries the pre-#1138text-white plus dark:bg-destructive/60), so this is a trap rather than a live failure. It springs on the first buttonClass({ variant: 'destructive' }), or on the next webjs ui add button, which would bring in the current kit source that honours text-destructive-foreground.
Both were found by a repo-wide sweep during #1138 / #1139, which fixed the same class of bug in the kit itself.
Design / approach
Defect 1 is the priority. Give light mode its own --destructive, deep enough to read on white, in BOTH light blocks (the forced [data-theme='light'] and the prefers-color-scheme: light default). The kit's own light value, oklch(0.52 0.20 25), measures 6.12:1 on white and is a reasonable starting point, though the blog has its own warmer palette so match it by eye and then verify.
Defect 2 is the same fix #1139 applied to the kit: keep the dark red (its live text-on-card role needs it light) and flip the dark foreground to a dark red so a filled variant would be legible.
Verify with the maths, not by eye. packages/ui/test/destructive-contrast.test.js has a reusable oklch-to-sRGB plus WCAG implementation, and its pairings() list is the set that matters here too.
Then decide whether to refresh examples/blog/components/ui/button.ts and badge.ts from the current kit. They are the shadcn "you own it" copies, so lagging is legitimate, but leaving them means the dogfood app demonstrates a pattern the kit no longer ships. webjsui diff shows the drift.
Implementation notes (for the implementing agent)
Where to edit:
examples/blog/app/layout.ts around L161-162 for the dark values, plus the two light blocks at ~L187 and ~L210, which currently inherit --destructive and must not.
Optionally examples/blog/components/ui/button.ts L39-40 and badge.ts, if refreshing the owned copies.
Landmines / gotchas:
The dark --destructive is load-bearing for the live error alerts in dark mode (6.88:1). Darkening it globally to suit white would fix light and break dark. This token genuinely needs a per-theme value, which is the whole reason the bug exists.
Check BOTH light blocks. Fixing only [data-theme='light'] leaves the OS-preference default broken, and that is the path most visitors take.
Do not verify by screenshot alone. 2.91:1 looks acceptable to most viewers, which is precisely why it shipped.
Invariants to respect:
Repo root AGENTS.md invariant 11 for any prose in comments.
packages/ui/AGENTS.md invariant 5 if the owned component copies are refreshed. Its list of deliberate divergences from shadcn is now recorded there.
Tests + docs surfaces:
examples/blog has smoke coverage under test/examples/. Consider whether the kit's contrast assertion should generalize to app palettes rather than being duplicated per app.
Problem
examples/bloghas two separate destructive-colour defects. One is live and user-facing.1. Live: error messages are 2.91:1 in light mode.
The palette declares
--destructiveonce, in the dark-default:rootblock:Neither light override redeclares it (
:root[data-theme='light']at L187, nor the@media (prefers-color-scheme: light)block at L210), but both DO set--card: oklch(1 0 0). So light mode paints a red chosen for a near-black surface onto a white card.alertClass({ variant: 'destructive' })isbg-card text-destructive …(examples/blog/components/ui/alert.ts:37), which puts that red text on the white card at 2.91:1, against the 4.5:1 minimum. It renders on real user paths:examples/blog/modules/auth/components/auth-forms.ts:106, the failed-login messageexamples/blog/modules/posts/components/new-post.ts:76, the failed-post-submit messageAn error message is the worst possible place for low contrast, and a failed login is exactly when a user is already having trouble. Dark mode is fine (6.88:1); the bug is light-mode only, which is the usual way this escapes notice.
2. Dormant: the declared fill pair is 2.59:1.
Near-white text on that light red is 2.59:1. Nothing renders it today (nothing in the app uses a filled destructive variant, and the app's owned
components/ui/button.ts:40still carries the pre-#1138text-whiteplusdark:bg-destructive/60), so this is a trap rather than a live failure. It springs on the firstbuttonClass({ variant: 'destructive' }), or on the nextwebjs ui add button, which would bring in the current kit source that honourstext-destructive-foreground.Both were found by a repo-wide sweep during #1138 / #1139, which fixed the same class of bug in the kit itself.
Design / approach
Defect 1 is the priority. Give light mode its own
--destructive, deep enough to read on white, in BOTH light blocks (the forced[data-theme='light']and theprefers-color-scheme: lightdefault). The kit's own light value,oklch(0.52 0.20 25), measures 6.12:1 on white and is a reasonable starting point, though the blog has its own warmer palette so match it by eye and then verify.Defect 2 is the same fix #1139 applied to the kit: keep the dark red (its live text-on-card role needs it light) and flip the dark foreground to a dark red so a filled variant would be legible.
Verify with the maths, not by eye.
packages/ui/test/destructive-contrast.test.jshas a reusable oklch-to-sRGB plus WCAG implementation, and itspairings()list is the set that matters here too.Then decide whether to refresh
examples/blog/components/ui/button.tsandbadge.tsfrom the current kit. They are the shadcn "you own it" copies, so lagging is legitimate, but leaving them means the dogfood app demonstrates a pattern the kit no longer ships.webjsui diffshows the drift.Implementation notes (for the implementing agent)
Where to edit:
examples/blog/app/layout.tsaround L161-162 for the dark values, plus the two light blocks at ~L187 and ~L210, which currently inherit--destructiveand must not.examples/blog/components/ui/button.tsL39-40 andbadge.ts, if refreshing the owned copies.Landmines / gotchas:
--destructiveis load-bearing for the live error alerts in dark mode (6.88:1). Darkening it globally to suit white would fix light and break dark. This token genuinely needs a per-theme value, which is the whole reason the bug exists.[data-theme='light']leaves the OS-preference default broken, and that is the path most visitors take.public/tailwind.css. Rebuild it. A dev server serving a stale stylesheet makes a colour change look like it did nothing, which cost real time on fix(ui): make the destructive red legible in dark mode #1139.Invariants to respect:
packages/ui/AGENTS.mdinvariant 5 if the owned component copies are refreshed. Its list of deliberate divergences from shadcn is now recorded there.Tests + docs surfaces:
examples/bloghas smoke coverage undertest/examples/. Consider whether the kit's contrast assertion should generalize to app palettes rather than being duplicated per app..agents/skills/webjs/references/styling.mdnow documents the fill-plus-foreground pairing rule and the per-theme flip, so the recipe this app diverged from is correct as of fix(ui): make the destructive red legible in dark mode #1139.Acceptance criteria
--destructive/--destructive-foregroundpair clears 4.5:1 in both themesbutton.ts/badge.tscopies are refreshed from the kit or deliberately left behind