Skip to content

[babel-plugin] Keep custom property rules inside @layer with useLayers#1752

Open
ryanda9910 wants to merge 1 commit into
facebook:mainfrom
ryanda9910:fix/1611-custom-props-escape-layer
Open

[babel-plugin] Keep custom property rules inside @layer with useLayers#1752
ryanda9910 wants to merge 1 commit into
facebook:mainfrom
ryanda9910:fix/1611-custom-props-escape-layer

Conversation

@ryanda9910

Copy link
Copy Markdown

What changed / motivation ?

When useCSSLayers is enabled, stylex.create() rules that set CSS custom properties (e.g. '--card-padding': '16px') could be emitted outside any @layer, making them impossible to override from external CSS layers.

Root cause

Custom properties get priority 1 (via getAtRulePriority), which floors into priorityLevel Math.floor(1 / 1000) === 0. That is the same priorityLevel bucket as the priority-0 at-rules @property, @keyframes and @position-try.

The layer-wrapping decision in processStylexRules used the priority of the first rule in the sorted group:

const pri = group[0][2];
return useLayers && pri > 0 ? `@layer ${layerName(index)}{...}` : collectedCSS;

Rules are sorted by priority ascending, so whenever a priority-0 at-rule (e.g. a @keyframes from animationName) sorted ahead of a custom property in the same group, pri was 0, pri > 0 was false, and the entire group — including the custom property — was emitted unlayered.

The pri > 0 guard was originally added in #1071 to keep @property / @keyframes / @position-try out of @layer (some CSS validators don't understand those at-rules inside a layer). It was never meant to exclude custom properties.

Because unlayered CSS always beats layered CSS in the cascade, external @layer rules could never override StyleX custom property assignments — defeating the point of useCSSLayers.

Before (custom property escapes the layer):

@layer priority1, priority2;
@keyframes fade{...}
.x1vtiyio{--card-padding:16px}   /* unlayered — @layer priority1 is declared but empty */
@layer priority2{
.color-x1e2nbdu{color:red}
}

After:

@layer priority1, priority2;
@keyframes fade{...}
@layer priority1{
.x1vtiyio{--card-padding:16px}
}
@layer priority2{
.color-x1e2nbdu{color:red}
}

Fix

Partition each group by layerability: priority-0 at-rules stay unlayered (preserving #1071), while priority > 0 rules — including custom properties, :root variable declarations and theme overrides — are wrapped in their layer. The decision is made per-rule instead of from group[0], so a leading at-rule can no longer drag layerable rules out of their layer.

Linked PR/Issues

Fixes #1611

Additional Context

  • Added a regression test in transform-process-test.js that reproduces the exact scenario (a custom property alongside a priority-0 @keyframes in the same priorityLevel) and asserts the @keyframes stays unlayered while the custom property is wrapped in @layer priority1.
  • The 8 existing useLayers snapshots in transform-process-test.js were updated: the priority-0 at-rules (@property, @keyframes) still print unlayered, and the previously-leaking :root var declarations / theme overrides / custom property rules are now correctly wrapped in @layer priority1 (which was previously declared in the header but empty).

Verified locally (Node 22):

  • jest (babel-plugin): 960 passed, 858 snapshots passed
  • flow check: No errors
  • eslint + prettier --check on changed files: clean

Pre-flight checklist

  • I have read the contributing guidelines
  • Performed a self-review of my code

CSS custom properties (priority 1) share a `priorityLevel`
(Math.floor(priority / 1000) === 0) with the priority-0 at-rules
`@property`, `@keyframes` and `@position-try`. The layer-wrapping
decision used `group[0][2]` (the priority of the first rule in the
sorted group), so a priority-0 at-rule sorting ahead of a custom
property made `pri > 0` false and pulled the whole group -- including
the custom property -- out of its `@layer`.

Unlayered CSS always wins the cascade over layered CSS, so external
`@layer` rules could never override StyleX-generated custom property
assignments, defeating the purpose of `useCSSLayers`.

Partition each group so priority-0 at-rules stay unlayered (as intended
by facebook#1071) while priority > 0 rules -- including custom properties -- are
wrapped in their layer.

Fixes facebook#1611
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

@ryanda9910 is attempting to deploy a commit to the Meta Open Source Team on Vercel.

A member of the Team first needs to authorize it.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 4, 2026
@ryanda9910 ryanda9910 marked this pull request as ready for review July 4, 2026 04:02
@ryanda9910 ryanda9910 requested review from mellyeliu and nmn as code owners July 4, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant