Skip to content

[Pro 1.7.0 / OSS 1.12.0] ios:/android: variants leak into the other platform's bundle with Tailwind 4.3.3 — only the first class per hoisted @media block keeps its platform #668

Description

@invivek26

What happened?

With Tailwind 4.3.3 (which Pro 1.7.0 / OSS 1.12.0 now pin via @tailwindcss/node), ios: / android: / native: classes leak into the other platform's bundle. Only the first class inside each @media ios { … } / @media android { … } block keeps its platform tag; every other class in the block compiles as an unconditional style and is emitted in both the iOS and Android stylesheets.

In our app (Pro 1.7.0, upgraded from 1.6.0 yesterday) this shows as e.g. ios:underline ios:w-screen-content android:border-b android:border-foreground on a TextInput rendering both the iOS text underline and the Android full-width bottom border on iOS, and the iOS full-width sizing plus text underline on Android. ios:font-… android:font-… pairs are affected the same way. Nothing changed in our class strings; the regression came purely from the Tailwind bump inside uniwind.

Theme blocks (@media (prefers-color-scheme: …)) are unaffected because they contain a single :root child.

Root cause: Tailwind 4.3.3 hoists variants. 4.3.2 emitted .ios\:underline { @media ios { … } } per class; 4.3.3 emits one @media ios { .ios\:w-4 {…} .ios\:underline {…} } block. In packages/uniwind/src/bundler/css-processor/processor.ts the rule.type === 'media' branch pushes the media queries once, then does this.declarationConfig = this.getDeclarationConfig() after each child rule, so children 2..N see an empty mediaQueries and never get platform set. addMetaToStylesTemplate then keeps them for every platform. This is the sibling of #661 / #662 — that fix handled the flattened selector half of the 4.3.3 change; this is the hoisted at-rule half.

Fix (verified against the repro; full native output correct on both platforms):

if (rule.type === 'media') {
    const { mediaQueries } = rule.value.query
    const inherited = [...this.declarationConfig.mediaQueries, ...mediaQueries]

    rule.value.rules.forEach(rule => {
        this.declarationConfig = { ...this.getDeclarationConfig(), mediaQueries: [...inherited] }
        this.parseRuleRec(rule)
    })
    this.declarationConfig = this.getDeclarationConfig()

    return
}

Happy to open a PR with this plus a ProcessorBuilder test feeding the hoisted shape if that helps.

Steps to Reproduce

  1. git clone https://github.com/invivek26/uniwind-platform-variant-leak-repro && cd uniwind-platform-variant-leak-repro
  2. bun install (resolves uniwind 1.12.0 + @tailwindcss/node 4.3.3; the same processor code ships in Pro 1.7.0)
  3. node repro.mjs — compiles global.css through uniwind's own compileCSS for ios and android and lists which platform-variant classes land in each stylesheet

Actual:

== ios bundle stylesheet contains:
  ios:w-4 (platform-tagged=true)
  ios:underline (platform-tagged=false)      <- lost its platform
  android:py-2 (platform-tagged=false)       <- android class shipped to iOS
  native:mt-1 (platform-tagged=true)
  native:mb-2 (platform-tagged=false)
== android bundle stylesheet contains:
  ios:underline (platform-tagged=false)      <- ios class shipped to Android
  android:border-b (platform-tagged=true)
  android:py-2 (platform-tagged=false)
  native:mt-1 (platform-tagged=true)
  native:mb-2 (platform-tagged=false)

Control: forcing Tailwind back to 4.3.2 with overrides on the same uniwind 1.12.0 gives the expected output (every class tagged, no cross-platform entries) — see the README.

Snack or Repository Link (Optional)

https://github.com/invivek26/uniwind-platform-variant-leak-repro

Uniwind version

Pro 1.7.0 (reproduced on OSS 1.12.0 as well — same processor)

React Native Version

0.86.3

Platforms

Android, iOS

Expo

Yes (SDK 57)

Additional information 〰

Activity

  1. Brentlok commented on Sep 7, 2026

    @Brentlok
    Contributor

    I've changed label to oss since it affects both versions

  2. Brentlok commented on Sep 7, 2026

    @Brentlok
    Contributor

    Happy to open a PR with this plus a ProcessorBuilder test feeding the hoisted shape if that helps.

    Feel free to open PR with fix 🙌
    I'm assigning this to you then, thanks in advance

  3. phoique commented on Sep 7, 2026

    @phoique

    Yes, I am bug a similar issue. I resolved it by downgrading the versions of uniwind (1.12.0 =>1.11.0) and tailwindcss (4.3.3 =>4.3.2).

  4. invivek26 commented on Sep 7, 2026

    @invivek26
    Author

    Just created a pr with the fix @Brentlok 🫡

  5. romanlesnikov commented on Sep 29, 2026

    @romanlesnikov

    Same root cause also breaks responsive breakpoints, not just platform variants.
    With the hoisted @media blocks, sm:/lg:/xl: after the first in a block compile
    with minWidth: 0 and apply at every width. In our app tailwindcss 4.3.2 emitted
    74 separate @media (width >= 40rem) blocks and 4.3.3 emitted one, so phones
    started rendering the desktop layout. The same one-line fix resolves it.

    Quick way to check a project without building to a device: compile the CSS
    through @tailwindcss/node, run it through uniwind's ProcessorBuilder with
    { platform: 'native' }, and read stylesheets['sm:hidden'][0].minWidth — 0 means
    the breakpoint was stripped.

  6. Brentlok commented on Sep 29, 2026

    @Brentlok
    Contributor

    Same root cause also breaks responsive breakpoints, not just platform variants. With the hoisted @media blocks, sm:/lg:/xl: after the first in a block compile with minWidth: 0 and apply at every width. In our app tailwindcss 4.3.2 emitted 74 separate @media (width >= 40rem) blocks and 4.3.3 emitted one, so phones started rendering the desktop layout. The same one-line fix resolves it.

    Quick way to check a project without building to a device: compile the CSS through @tailwindcss/node, run it through uniwind's ProcessorBuilder with { platform: 'native' }, and read stylesheets['sm:hidden'][0].minWidth — 0 means the breakpoint was stripped.

    #686 fixes that as well

  7. github-actions commented on Oct 1, 2026

    @github-actions
    Contributor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions