facebook / facebook/stylex

Bug: CSS custom property styles escape `@layer` when `useCSSLayers: true`

Open
#1,611 1 comment 0 reactions 0 assignees View on GitHub
bug
Dominant language
JavaScript
Stars
10.3k
Forks
481
Avg merge
3d 8h
Merged PRs (30d)
13

Description

### Describe the issue

## Bug: CSS custom property styles escape `@layer` when `useCSSLayers: true`

### Summary

When `useCSSLayers: true` is set, `stylex.create()` rules that set CSS custom properties (e.g. `'--my-var': 'value'`) are emitted **outside** any `@layer`, making them impossible to override from external CSS layers.

### Root Cause

In `processStylexRules` ([babel-plugin/lib/index.js](https://github.com/nicolo-ribaudo/comment-parser/blob/main/src/index.js)):

1. `getPriority('--anything')` returns **1** (correct — custom properties are intentionally prioritized above shorthands at 0)
2. Rules are grouped by `Math.floor(priority / 1000)` → priority 1 maps to **priorityLevel 0**
3. PriorityLevel 0 also contains all shorthand/base properties (priority 0)
4. The layer-wrapping check uses the **first rule's raw priority** in the group:
```js
const pri = group[0][2];
return useLayers && pri > 0 ? `@layer ${layerName(index)}{...}` : collectedCSS;
```
5. Since priority-0 rules sort before priority-1 rules, `pri` is always **0** for this group
6. The entire priorityLevel-0 group — including custom property rules — is emitted **unlayered**

### Impact

Unlayered CSS always beats layered CSS in the cascade, regardless of layer ordering. This means:

- External `@layer` rules (e.g. theme overrides, design system layers) **can never override** StyleX-generated custom property assignments
- This defeats the purpose of `useCSSLayers`, which exists to allow external CSS to participate in cascade ordering with StyleX output

### Reproduction

```js
// component.tsx
import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
card: {
'--card-padding': '16px', // priority 1 → should be in a layer
color: 'red', // priority 2000+ → in a layer ✓
},
});
```

```css
/* Expected output with useCSSLayers: true */
@layer priority1 {
.x1abc { --card-padding: 16px; }
.x2def { color: red; }
}

/* Actual output */
.x1abc { --card-padding: 16px; } /* ← unlayered! */
@layer priority1 {
.x2def { color: red; }
}
```

External CSS in any `@layer` cannot override `--card-padding`:

```css
/* This loses to the unlayered StyleX output, regardless of layer order */
@layer my-theme {
.card { --card-padding: 20px; }
}
```

### Suggested Fix

The simplest fix: when `useCSSLayers` is true, wrap **all** groups in layers, including priorityLevel 0. The `pri > 0` guard was likely intended to skip `defineVars` output (`:root` declarations), but those are filtered into `constantRules` earlier in the function and don't reach this code path.

```diff
- return useLayers && pri > 0 ? `@layer ${layerName(index)}{\\n${collectedCSS}\\n}` : collectedCSS;
+ return useLayers ? `@layer ${layerName(index)}{\\n${collectedCSS}\\n}` : collectedCSS;
```

### Environment

- `@stylexjs/babel-plugin`: (checked against source in npm)
- `@stylexjs/unplugin`: used with Vite + `useCSSLayers: true`

### Expected behavior

```css
/* Expected output with useCSSLayers: true */
@layer priority1 {
.x1abc { --card-padding: 16px; }
.x2def { color: red; }
}
```
```css
/* Actual output */
@layer priority1 {
.x2def { color: red; }
}
.x1abc { --card-padding: 16px; } /* ← unlayered! */
```

### Steps to reproduce

### Reproduction

```js
// component.tsx
import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
card: {
'--card-padding': '16px', // priority 1 → should be in a layer
color: 'red', // priority 2000+ → in a layer ✓
},
});
```

Using Vite with `useCSSLayers: true`.

### Test case

### Reproduction

```js
// component.tsx
import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
card: {
'--card-padding': '16px', // priority 1 → should be in a layer
color: 'red', // priority 2000+ → in a layer ✓
},
});
```

```css
/* Expected output with useCSSLayers: true */
@layer priority1 {
.x1abc { --card-padding: 16px; }
.x2def { color: red; }
}

/* Actual output */
.x1abc { --card-padding: 16px; } /* ← unlayered! */
@layer priority1 {
.x2def { color: red; }
}
```

External CSS in any `@layer` cannot override `--card-padding`:

```css
/* This loses to the unlayered StyleX output, regardless of layer order */
@layer my-theme {
.card { --card-padding: 20px; }
}
```

### Additional comments

See P2270190221 for full internal output of XDS's storybook generated CSS to see the issue in prod.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.