Skip to content

Commit c92543b

Browse files
sgammonclaude
andcommitted
fix(ui): bind the dark: variant to the .dark class
Tailwind v4's default dark: variant is a prefers-color-scheme media query, and styles.css never overrode it — so every dark: utility in the catalog (select, switch, tabs, dropdown-menu, and ElideLogo's raster theme swap) followed the OS rather than the site theme. A user who toggled to light while their OS was dark got dark-mode styling. The token layer was unaffected because it keys off .dark directly, which is why this stayed hidden. Found by rendering the raster theme swap in a real browser: both themes showed the light cut. Guarded by a play function on the RasterThemeSwap story, which asserts computed display under and outside .dark — the only place this is checkable, since jsdom reads the class names but never evaluates them. Verified the guard fails with the variant removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent df1cd48 commit c92543b

2 files changed

Lines changed: 46 additions & 0 deletions

File tree

packages/ui/src/stories/ElideLogo.stories.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Meta, StoryObj } from "@storybook/react-vite";
2+
import { expect } from "storybook/test";
23
import { ElideLogo } from "../components/elide-logo";
34
import { AppNav } from "../components/app-nav";
45

@@ -148,6 +149,41 @@ export const RasterThemeSwap: Story = {
148149
</Cell>
149150
</div>
150151
),
152+
/**
153+
* Asserts the swap in real CSS, which is the only place it can be checked:
154+
* jsdom sees the `dark:hidden` / `hidden dark:block` class names but never
155+
* evaluates them, so a unit test passes even when the variant is misconfigured
156+
* and both themes show the light cut. That exact bug shipped once — Tailwind
157+
* v4's default `dark:` is a `prefers-color-scheme` media query, so it ignored
158+
* the `.dark` class until `styles.css` bound it with `@custom-variant`.
159+
*/
160+
play: async ({ canvasElement }) => {
161+
const root = document.documentElement;
162+
const wasDark = root.classList.contains("dark");
163+
// Queried by selector, not by role: the dark cut carries alt="" so the pair
164+
// is announced once, which also makes it presentational and invisible to
165+
// getAllByRole("img").
166+
const shown = (src: string) => {
167+
const img = canvasElement.querySelector<HTMLImageElement>(`img[src$="${src}"]`);
168+
if (!img) throw new Error(`no <img> for ${src}`);
169+
return getComputedStyle(img).display !== "none";
170+
};
171+
172+
try {
173+
root.classList.remove("dark");
174+
await expect(shown("elide-full-blend-light.webp")).toBe(true);
175+
await expect(shown("elide-full-blend-dark.webp")).toBe(false);
176+
177+
root.classList.add("dark");
178+
await expect(shown("elide-full-blend-light.webp")).toBe(false);
179+
await expect(shown("elide-full-blend-dark.webp")).toBe(true);
180+
181+
// The theme-agnostic gradient stays visible either way.
182+
await expect(shown("elide-full-gradient.webp")).toBe(true);
183+
} finally {
184+
root.classList.toggle("dark", wasDark);
185+
}
186+
},
151187
};
152188

153189
/** Width always follows the artwork's aspect ratio; nothing is ever stretched. */

packages/ui/src/styles.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111
* (animate-in, fade-in-0, zoom-in-95, slide-in-from-*). */
1212
@import "tw-animate-css";
1313

14+
/* Bind the `dark:` variant to the `.dark` class on <html>, which is how
15+
* ThemeProvider (and the Storybook decorator) applies the theme.
16+
*
17+
* Tailwind v4's default `dark:` is `@media (prefers-color-scheme: dark)`, so
18+
* without this every `dark:` utility in the catalog silently follows the OS
19+
* instead of the site theme — a user who toggles to light while their OS is
20+
* dark gets dark-mode styling. The token layer was unaffected (it keys off
21+
* `.dark` directly), which is why this stayed hidden. */
22+
@custom-variant dark (&:where(.dark, .dark *));
23+
1424
/* Tailwind v4 auto-detection roots at this file's location. When consumed via
1525
* the `node_modules/@elide/ui` symlink (Storybook, and any downstream app), that
1626
* tree is gitignored, so auto-detection finds nothing and no component utilities

0 commit comments

Comments
 (0)