Skip to content

rtl:/ltr: variants silently dropped — lightningcss downcompiles :dir() to :lang() which is unhandled #453

Description

@Noitidart

Describe the bug

The rtl: and ltr: variants from Tailwind CSS v4 are silently dropped during compilation. No error is produced — the styles simply never apply. Related to nativewind/nativewind#1433 but traced to a different root cause in the react-native-css v5 compiler.

Root cause

  1. Tailwind v4 generates :dir(rtl) in the rtl: variant (confirmed in tailwindcss source)
  2. Lightningcss 1.30.1 downcompiles :dir(rtl) to :is(:lang(ae), :lang(ar), ...) at the parse level
  3. parseIsWhereComponents in selector-builder.ts has a case "dir" handler but no case "lang" handler
  4. :lang() hits default: { return null; } → returns null
  5. Inside :where(), [dir="rtl"] attribute selectors also return null (line ~397)
  6. ALL alternatives produce nothing → the entire rule is silently dropped

Key finding: Features.DirSelector does not help

Adding Features.DirSelector to lightningcss include list does NOT prevent the downcompile. Confirmed with a console.log verifying the flag IS loaded — the CSS output still contains :is(:lang(...)) instead of :dir(rtl). The downcompile happens at the parse level, before feature flags are consulted.

Proposed fix

Add a case "lang" handler to parseIsWhereComponents. Lightningcss expands :dir(rtl) into a fixed set of 19 RTL languages. Map these back to [=, "dir", "rtl"], leveraging the existing runtime case "dir" check against I18nManager.isRTL:

case "lang": {
  const RTL_LANGUAGES = new Set([
    "ae","ar","arc","bcc","bqi","ckb","dv","fa","glk",
    "he","ku","mzn","nqo","pnb","ps","sd","ug","ur","yi"
  ]);
  const direction = component.languages?.[0] && RTL_LANGUAGES.has(component.languages[0]) ? "rtl" : "ltr";
  queries ??= [{ specificity: [] }];
  queries.forEach(query => {
    getMediaQuery(query).push(["=", "dir", direction]);
  });
  return parseIsWhereComponents(type, selector, index + 1, queries);
}

Reproduction

Steps:

  1. npm install && npx expo run:ios
  2. Press "Force RTL" button
  3. Fully restart the app
  4. The rtl:bg-amber-400 class should make the background amber, but it stays blue

Expected behavior

rtl:bg-amber-400 should apply background-color: amber-400 when I18nManager.isRTL === true.

Workaround

Use patch-package with the patch from the fix branch.

Environment

  • react-native-css: 3.0.7
  • nativewind: 5.0.0-preview.4
  • react-native: 0.86.0
  • expo: 57

Activity

  1. github-actions commented on Jul 22, 2026

    @github-actions

    ❓ Inconclusive

    Our automated reproduction pipeline could not conclusively judge this bug. See the CI run for the captured evidence. A maintainer will take a closer look.

    Cannot assess whether the bug reproduces because the iOS build completed but failed to bundle the JavaScript code (main.jsbundle missing). The app would not run the user's test case with the rtl:bg-amber-400 class, making it impossible to observe whether the variant is silently dropped as reported.

    Platform Verdict Evidence
    ios inconclusive repro_status indicates 'bundle-missing' — no main.jsbundle was embedded, so user JS would not run. Build succeeded but the app cannot execute the reproduction test case.

    Automated by .github/workflows/issue-repro.yml using agent-device. Workflow run: https://github.com/nativewind/nativewind/actions/runs/29952432645

  2. Abdalkaderdev commented on Oct 8, 2026

    @Abdalkaderdev

    This no longer reproduces on main. I checked with @tailwindcss/postcss through the repo's own test harness:

    • rtl:text-white → { color: '#fff' } when I18nManager.isRTL is true, nothing when false
    • ltr:text-white → the inverse

    So the :lang() form that lightningcss downcompiles :dir() into is being handled somewhere now, and parseIsWhereComponents no longer drops the rule.

    What was missing was anything to keep it fixed — the existing ltr: test never sets isRTL, so it passes even if the condition is dropped and the style applies unconditionally, and there was no rtl: test. Opened #472 with both directions and the negative case.

    Worth confirming on your repro branch before closing, in case your setup differs — your repro pins react-native-css 3.0.7 and this was against main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions