#1856·stylex

[babel-plugin] processStylexRules corrupts @position-try rules with RTL ancestor selectors

Author: gyoonCreated Sep 8, 2026Updated Sep 8, 2026
Labelsbug

Describe the issue

When enableLTRRTLComments is disabled (the default), processStylexRules corrupts @position-try at-rules by injecting RTL ancestor selectors (html:not([dir='rtl']) / html[dir='rtl']) into the rule body. The resulting CSS is invalid and breaks the build — for example, lightningcss raises a SquareBracketBlock parse error.

The root cause is that addAncestorSelector treats every non-@keyframes at-rule the same way it treats @media/@supports/@container — it assumes the part after the at-rule header contains CSS selectors and prepends the ancestor selector. But @position-try (like @keyframes) has a name followed by a declaration block with no selector, so the ancestor selector is injected directly into the declaration list:

Before:  @position-try --xhul0o3 {height:100px;top:0;width:100px;}
After:   @position-try --xhul0o3 {html[dir='rtl'] height:100px;top:0;width:100px;}

The html[dir='rtl'] token is not a valid declaration, and the [ triggers a SquareBracketBlock parse error.

The same issue affects addSpecificityLevel, which injects :not(#\#) into the @position-try block when useLayers is disabled:

After:   @position-try --xhul0o3 :not(#\#){height:100px;top:0;width:100px;}

Expected behavior

@position-try should be skipped by addAncestorSelector and addSpecificityLevel just like @keyframes, since its block contains declarations, not selectors. processStylexRules already documents this intent ("Don't put @property, @keyframe, @position-try in layers") but does not apply the same exemption to ancestor-selector/specificity injection.

Steps to reproduce

  1. Create a file that uses stylex.positionTry(...) and references it via positionTryFallbacks:
javascript
import * as stylex from '@stylexjs/stylex';

const popup = stylex.positionTry({
  positionArea: 'bottom span-right',
});

const styles = stylex.create({
  popup: {
    positionArea: 'bottom span-right',
    positionTryFallbacks: popup,
  },
});
  1. Build the CSS with processStylexRules using the default config (enableLTRRTLComments: false), e.g. via @stylexjs/unplugin with useCSSLayers: true.

  2. Observe the emitted @position-try rule: the RTL ancestor selector (html[dir='rtl']) is injected into the declaration block, producing invalid CSS.

Environment:

  • @stylexjs/babel-plugin 0.19.0 (latest)
  • OS: Linux

Test case

javascript
import stylexPlugin from '@stylexjs/babel-plugin';

const rules = [[
  '--xhul0o3',
  {
    ltr: '@position-try --xhul0o3 {height:height;height:100px;top:top;top:0;width:width;width:100px;}',
    rtl: '@position-try --xhul0o3 {height:100px;top:0;width:100px;}',
  },
  0,
]];

const css = stylexPlugin.processStylexRules(rules, { enableLTRRTLComments: false });
// Expected: the @position-try rule is emitted unchanged.
// Actual (before fix): "@position-try --xhul0o3 {html[dir='rtl'] height:100px;top:0;width:100px;}"

Additional comments

A proposed fix is to exempt @position-try from both addAncestorSelector and addSpecificityLevel, exactly as @keyframes already is — see PR #1854.