Repository navigation
[add] StyleSheet.setCascadeLayer to nest the generated CSS in a cascade layer - #2858
Open
YevheniiKotyrlo wants to merge 2 commits into
Open
YevheniiKotyrlo wants to merge 2 commits into
YevheniiKotyrlo wants to merge 2 commits into
Conversation
|
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:
|
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
force-pushed
the
fix/layer-runtime-stylesheet
branch
from
October 14, 2026 06:57
ecf23ce to
e0d1610
Compare
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
marked this pull request as draft
October 16, 2026 11:31
YevheniiKotyrlo
marked this pull request as ready for review
October 16, 2026 11:32
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The CSS
StyleSheetgenerates 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 aVieworTextsets, such asText'sfont: '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 throwCannot 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 layername: the rules already inserted move into it, every later rule goes into it, andgetStyleElement()andgetSheet()serialize the sheet in it, so a server-rendered sheet hydrates into the same layer. The app positions the layer among its own: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
StyleSheetstyle.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;setCascadeLayermoving 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.jsandAppRegistry'sindex-test.node.js: the server-rendered sheet andgetStyleElement()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'sdom/index.js,createOrderedCSSStyleSheet.jsandStyleSheet/index.js, all 16 new cases fail, the declared-layer ones by throwing in hydration.Verification
unit:dom736 passed, 6 skipped ·unit:node8 passed ·flow0 errors ·lint·formatclean. The React canary job was not run.The built
distin 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:border-top-widthmaster1pxsetCascadeLayer('rnw')after the first rules@layer rnw5px@layer rnw{}5px@layer rnw5pxIn the last three rows a shadow root's copy opens with
@layer rnwin all three engines.No issue tracks this; I searched the issues for "cascade layer", "@layer", "css layer" and "tailwind".