Skip to content

Theme utilities silently dropped when a @theme namespace reset is imported after @import "uniwind" #623

Description

@melv-n

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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions