Skip to content

[add] StyleSheet.setCascadeLayer to nest the generated CSS in a cascade layer - #2858

Open
YevheniiKotyrlo wants to merge 2 commits into
necolas:masterfrom
YevheniiKotyrlo:fix/layer-runtime-stylesheet
Open

YevheniiKotyrlo wants to merge 2 commits into
necolas:masterfrom
YevheniiKotyrlo:fix/layer-runtime-stylesheet

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Sep 10, 2026 •

Copy link
Copy Markdown

Problem

The CSS StyleSheet generates is unlayered, and an unlayered declaration takes precedence over every layered one, whatever its specificity. An app that orders its own CSS in cascade layers (Tailwind v4 puts its utilities in @layer utilities) can't override a style a View or Text sets, such as Text's font: '14px System', from a layer. Nor can it put the generated CSS in a layer: declaring the style sheet's element with one, <style id="react-native-stylesheet">@layer rnw{}</style>, makes hydration throw Cannot read properties of undefined (reading 'rules'), because it reads a rule before the first group as a member of a group that was never opened.

Solution

StyleSheet.setCascadeLayer(name) nests the generated CSS in the layer name: the rules already inserted move into it, every later rule goes into it, and getStyleElement() and getSheet() serialize the sheet in it, so a server-rendered sheet hydrates into the same layer. The app positions the layer among its own:

StyleSheet.setCascadeLayer('rnw');
@layer rnw, base, components, utilities;

A page can instead declare the layer in the style sheet's element before the app loads. A copy of the sheet in a shadow root or an iframe is nested in the same layer. Nothing changes for an app that does neither: layering by default would let an app's unlayered CSS, such as a global reset, override every StyleSheet style.

Hydration leaves rules a page declared before the first group alone instead of throwing. An engine without cascade layers rejects the layer block, and the rules stay where they were.

Tests

  • dom-test.js: inserting into a declared layer, serializing it, hydrating server-rendered rules from it and copying it into an iframe; setCascadeLayer moving the rules already inserted, keeping a page's own rules, leaving a sheet already in the layer alone, moving rules out of a layer of another name, nesting a sheet created after the call and the copies in other roots, and keeping the rules in place where an engine rejects or misparses the layer.
  • dom-test.node.js and AppRegistry's index-test.node.js: the server-rendered sheet and getStyleElement() in the layer.
  • dom-cascadeLayer-test.js: finding a declared layer, a nested and an anonymous name, and skipping other rules.
  • dom-createOrderedCSSStyleSheet-test.js: hydration with a page's rule before the first group.

With master's dom/index.js, createOrderedCSSStyleSheet.js and StyleSheet/index.js, all 16 new cases fail, the declared-layer ones by throwing in hydration.

Verification

unit:dom 736 passed, 6 skipped · unit:node 8 passed · flow 0 errors · lint · format clean. The React canary job was not run.

The built dist in Chromium 153, Firefox 155 and WebKit 26.6, with @layer app{.box{border-top-width:5px}} and the generated .r-box{border-top-width:1px} on one element:

top-level rules in the sheet border-top-width
no layer, as on master 6 1px
setCascadeLayer('rnw') after the first rules 1, @layer rnw 5px
the page declares @layer rnw{} 1 5px
the page declares server-rendered rules in @layer rnw 1, no rule duplicated 5px

In the last three rows a shadow root's copy opens with @layer rnw in all three engines.

No issue tracks this; I searched the issues for "cascade layer", "@layer", "css layer" and "tailwind".

@codesandbox-ci

codesandbox-ci Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 1d3cef2:

Sandbox Source
react-native-web-examples Configuration

The rules StyleSheet injects are framework defaults, but they are emitted
unlayered — and under CSS Cascade 5 an unlayered declaration takes precedence
over every layered one, regardless of specificity or layer order. That makes
them a ceiling rather than a default: an application that puts its own rules in
a cascade layer finds them silently outranked, with no error and no specificity
fight available to win. `!important` is the only author mechanism that reaches
past an unlayered rule, and using it inverts the model, since an important
declaration in a lower layer then beats every higher one.

Nesting the sheet's rules in a layer makes the defaults defaults again.
Application styles stay unlayered and continue to win.

`createOrderedCSSStyleSheet` consumes only `cssRules` and `insertRule`, both of
which a CSSLayerBlockRule provides with identical semantics, so grouping,
ordering, de-duplication and hydration are unchanged one level deeper. Its
parameter is now typed structurally to say so.

Support is feature-tested rather than assumed, because an engine without it
fails silently in two directions: `insertRule` may accept the layer text and
expose it as an ordinary style rule, which then corrupts the sheet's group
hydration, and a style element whose text is wrapped in a layer block may fail
to parse in full. Where cascade layers are unavailable the sheet is left exactly
as it was.

The serialized text `getSheet()` returns is wrapped as well, so a server
rendered sheet carries the same precedence as the runtime one and hydration has
a layer to read the rules back out of. Emitted unlayered it would outrank the
application's own layered rules until hydration replaced it, and every
server-rendered rule would then be duplicated into an empty layer.

The layer name is exported as `StyleSheet.cascadeLayerName`, so an application
can position it with an `@layer` statement rather than depending on the order
style elements happen to be inserted in.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/layer-runtime-stylesheet branch from ecf23ce to e0d1610 Compare October 14, 2026 06:57
Nesting the sheet in a layer of its own changed precedence for every app:
an app's unlayered rules, such as a global reset, would then override every
StyleSheet style. The rules are now nested only on request:
StyleSheet.setCascadeLayer(name) nests the rules already inserted, every
later rule and the server-rendered sheet, and a page can instead declare the
layer in the style sheet's element, e.g.
<style id="react-native-stylesheet">@layer rnw{}</style>. Every other app is
unchanged. A copy of the sheet in another root is nested in the same layer,
and hydration leaves rules the page declared before the first group alone
instead of throwing.
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as draft October 16, 2026 11:31
@YevheniiKotyrlo YevheniiKotyrlo changed the title [fix] Nest the runtime style sheet in a cascade layer [add] StyleSheet.setCascadeLayer to nest the generated CSS in a cascade layer Oct 16, 2026
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 16, 2026 11:32

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant