Using uniwind@1.10.0 with Expo + Metro.
We spent three rounds of debugging on what looked like a broken dark mode, and the actual cause was CSS import order. Uniwind is behaving as designed here, so this is a report about a silent failure rather than a bug, but I think it is worth a warning or a docs line because the setup that triggers it is the documented one.
What happens
Uniwind registers the variables it collects from @variant blocks by appending a @theme { --var: unset; } block to the generated artifact (src/bundler/artifacts/css/themes.ts:121-128). That block is what makes bg-canvas and similar exist as Tailwind utilities at all, since the actual values live in @layer theme rather than in @theme.
Tailwind v4's namespace reset (--color-*: initial) erases every registration in that namespace before it. So if the CSS entry is:
@import "tailwindcss";
@import "uniwind";
@import "./theme.css"; /* opens with @theme { --color-*: initial; } */
the reset in theme.css wipes uniwind's @theme block and Tailwind emits zero color, shadow, or other theme scoped utilities. There is no error and no warning. The "All themes must have the same variables" check at themes.ts:106-119 does not catch it, since the variables uniwind collected are complete; the loss happens afterwards inside Tailwind.
The import order in the docs (tailwindcss, then uniwind, then your theme) is the order that triggers this, and purging Tailwind's default palette is a normal thing to do in a design system, so following both lands you here.
Why it took us so long to find
It does not present as "nothing is styled". A className surface whose color utility resolved to nothing falls back to React Native defaults, which on a light background reads as a working light theme. So the symptom is "dark mode is broken", and light mode looks fine even though it was never using our tokens either.
Every runtime signal stays correct while this is happening. Uniwind.currentTheme, hasAdaptiveThemes, useUniwind() and UniwindStore.runtime.currentThemeName all report dark under dark appearance, Uniwind.setTheme('dark') transitions correctly and logs correctly, the generated CSS looks right, scopedVars contains the variables for both themes, and there is only one uniwind module instance. We checked all of that before thinking to look at the emitted utility set.
Reproduction
theme.css:
@theme {
--color-*: initial;
}
@layer theme {
:root {
@variant light { --color-canvas: oklch(0.96 0.012 78); }
}
:root {
@variant dark { --color-canvas: oklch(0.17 0.014 55); }
}
}
global.css:
@import "tailwindcss";
@import "uniwind";
@import "./theme.css";
Use className="bg-canvas" somewhere in scanned source, compile through the Metro transformer (compileCSS) and look at the emitted stylesheet.
Observed: no bg-canvas key. In our app that was 194 emitted utilities with not a single color utility among them, while --color-canvas was present in scopedVars for both __uniwind-theme-light and __uniwind-theme-dark and referenced by nothing.
Expected: bg-canvas present, resolving vars["--color-canvas"] with a StyleDependency.Theme dependency.
Moving @import "./theme.css" above @import "uniwind" fixes it. Same app, 241 utilities, full color and shadow set, correct theme at cold start and on live appearance change.
Suggestions
A warning would have been enough on its own. After the Tailwind build in compileTailwind, uniwind knows which variables it registered, and if none of them survived into the compiled output's @theme that can only be this misconfiguration. One line naming the fix would have saved us the whole investigation.
Failing that, a note on the Global CSS docs page that a @theme namespace reset has to be imported before @import "uniwind", and why.
Making the registration order independent would remove the sharp edge entirely, but that is obviously a bigger change and Tailwind's reset semantics are deliberate, so I would not assume it is the right call.
Happy to open a PR for the warning if that would help.
Using uniwind@1.10.0 with Expo + Metro.
We spent three rounds of debugging on what looked like a broken dark mode, and the actual cause was CSS import order. Uniwind is behaving as designed here, so this is a report about a silent failure rather than a bug, but I think it is worth a warning or a docs line because the setup that triggers it is the documented one.
What happens
Uniwind registers the variables it collects from
@variantblocks by appending a@theme { --var: unset; }block to the generated artifact (src/bundler/artifacts/css/themes.ts:121-128). That block is what makesbg-canvasand similar exist as Tailwind utilities at all, since the actual values live in@layer themerather than in@theme.Tailwind v4's namespace reset (
--color-*: initial) erases every registration in that namespace before it. So if the CSS entry is:the reset in
theme.csswipes uniwind's@themeblock and Tailwind emits zero color, shadow, or other theme scoped utilities. There is no error and no warning. The "All themes must have the same variables" check atthemes.ts:106-119does not catch it, since the variables uniwind collected are complete; the loss happens afterwards inside Tailwind.The import order in the docs (tailwindcss, then uniwind, then your theme) is the order that triggers this, and purging Tailwind's default palette is a normal thing to do in a design system, so following both lands you here.
Why it took us so long to find
It does not present as "nothing is styled". A
classNamesurface whose color utility resolved to nothing falls back to React Native defaults, which on a light background reads as a working light theme. So the symptom is "dark mode is broken", and light mode looks fine even though it was never using our tokens either.Every runtime signal stays correct while this is happening.
Uniwind.currentTheme,hasAdaptiveThemes,useUniwind()andUniwindStore.runtime.currentThemeNameall reportdarkunder dark appearance,Uniwind.setTheme('dark')transitions correctly and logs correctly, the generated CSS looks right,scopedVarscontains the variables for both themes, and there is only one uniwind module instance. We checked all of that before thinking to look at the emitted utility set.Reproduction
theme.css:global.css:Use
className="bg-canvas"somewhere in scanned source, compile through the Metro transformer (compileCSS) and look at the emitted stylesheet.Observed: no
bg-canvaskey. In our app that was 194 emitted utilities with not a single color utility among them, while--color-canvaswas present inscopedVarsfor both__uniwind-theme-lightand__uniwind-theme-darkand referenced by nothing.Expected:
bg-canvaspresent, resolvingvars["--color-canvas"]with aStyleDependency.Themedependency.Moving
@import "./theme.css"above@import "uniwind"fixes it. Same app, 241 utilities, full color and shadow set, correct theme at cold start and on live appearance change.Suggestions
A warning would have been enough on its own. After the Tailwind build in
compileTailwind, uniwind knows which variables it registered, and if none of them survived into the compiled output's@themethat can only be this misconfiguration. One line naming the fix would have saved us the whole investigation.Failing that, a note on the Global CSS docs page that a
@themenamespace reset has to be imported before@import "uniwind", and why.Making the registration order independent would remove the sharp edge entirely, but that is obviously a bigger change and Tailwind's reset semantics are deliberate, so I would not assume it is the right call.
Happy to open a PR for the warning if that would help.