A syntax proposal for Cascade Layers #4470. This does not include full discussion of the Cascade Layer purpose and use-cases, which can be found in the various linked issues, but attempts to answer many of the outstanding questions about how we might implement a layering feature.
Collaborators:
- Elika Etemad
- Florian Rivoal
- Miriam Suzanne
- Tab Atkins Jr.
Where do Cascade Layers fit in the cascade? #5003
- Origins & Importance
- Context (eg Shadow DOM)
- Layers
- Specificity
- Source Order
This avoids a lot of implementation complexity, while still giving authors a way to group and layer selectors. In some ways this is similar to adding a new top-level "specificity", but the interaction with !important
is a bit different.
How do Cascade Layers interact with Shadow DOM? #4984
Shadow DOM can be addressed in the cascade before layers are taken into account. We don't expect any particular issues with this interaction.
How do Cascade Layers interact with !important
? #4971
With Cascade Layers at a lower level than origins & importance, layered declarations will continue to divide into the existing origin structure and author layers:
- Transitions
- ❗️User Agent
- ❗️User
- ❗️Author
- Animations
- Author
- User
- User Agent
Within the two author origins, !important
declarations will follow origin-ordering logic: with !important
layers reversed in order. Unlayered styles rank highest in the normal origin, and lowest in the !important
origin.
- ❗️Author
- ❗️Reset
- ❗️Base
- ❗️Patterns
- ❗️Components
- ❗️unlayered
- Animations
- Author
- unlayered
- Components
- Patterns
- Base
- Reset
Layers provide the selector-override use-case that leads authors to misuse !important
. By making layers work the same as origins in this way, we are building on the original intent of the flag, and helping teach proper usage. Since authors are in control of the layering order, it should still be possible to generate any overrides necessary.
A final question is where does the style attribute fit in all this. In level 2 it was described through specificity and belonged to the normal author layer. In level 3, it is described through the scoping mechanism, but that's not implemented anywhere, so maybe it's time to re-explain it some other way. In order to remain backwards compatible, it needs to stay at the top of both Author origins:
- ❗️Author
- ❗️Style Attribute
- ❗️Defined Layers...
- ❗️unlayered
- Animations
- Author
- Style Attribute
- unlayered
- Defined Layers...
This can be described as a top Cascade Layer that is not reversed in the !important
origin, or as an additional independent stage in the cascade, between Context and Layers.
What are the proper "levels" for managing Cascade Layers? #4969
Much like media-queries, Cascade Layers can be managed using both a combination of file-import & at-rule syntax.
/* @layer <name>? { <contents> } */
@layer reset {
*, ::before, ::after { box-sizing: border-box; }
html { line-sizing: normal; }
body { margin: 0; }
}
@layer base {
/* site base styles */
}
@layer components {
/* site components styles */
}
@layer reset {
/* add to the reset layer from elsewhere */
}
Layers are stacked in order of their first appearance - with later-appearing layers stacked above (and overriding) previously defined layers. That builds on existing cascade rules and organizational best-practice, while also making it possible to use layers without any explicit stacking syntax.
If authors do want to re-order layers, they can do that already by listing empty layer blocks before any other layers:
@layer reset {}
@layer base {}
@layer bootstrap {}
@layer components {}
Still, it might be nice for authors to have an explicit shorthand to achieve the same outcome. We could add a @layers
rule that has exactly the same functionality:
@layers reset, base, bootstrap, components;
This would also help resolve the fact that @import
rules are required before all other CSS blocks. By allowing @layers
to appear before/between @import
rules, we can avoid any weird syntactic issues with allowing @layer
in that position only when empty.
The exact syntax here still needs some debate. The most direct approach would be allowing a url()
in place of a {…}
code block:
/* @layer <name>? url(<contents>) */
@layer reset url(reset.css);
@layer bootstrap url(bootstrap.css);
Other proposals include building on @import
, creating a new at-rule, or allowing imports to be nested inside the block syntax:
@import layer reset url(reset.css);
@layer-import reset url(reset.css);
@layer reset {
@import url(reset.css);
}
/* etc… */
In any case, this raises some question around ordering of @layer
and @import
rules -- which are currently required to come first. We may also want a syntax for HTML <link>
imports?
Providing both block & file-level syntax means block-layers from a file can be grouped and "nested" inside a named import-layer:
/* bootstrap.css */
@layer reset {}
@layer base {}
@layer components {}
/* …potentially also some unlayered code… */
/* site.css */
@layer bootstrap url(bootstrap.css);
This would generate a bootstrap
layer with reset
, base
, components
, and unlayered
sub-layers:
- Author
- unlayered
- Bootstrap
- unlayered
- Components
- Base
- Reset
That can be useful as a naming and organizing convention for authors, but the reality can be flattened without any change to the behavior:
- Author
- unlayered
- Bootstrap unlayered
- Bootstrap Components
- Bootstrap Base
- Bootstrap Reset
Using a well-designed library, and a syntax for nested-naming, authors could integrate their code with the existing library layers:
/* re-ordering "nested" layers: */
@layer bootstrap reset {};
@layer bootstrap base {};
@layer base {};
@layer bootstrap components {};
@layer components {};
/* importing & naming the bootstrap layers: */
@layer bootstrap url(bootstrap.css);
@layer bootstrap base {
/* add code at the bootstrap-base layer… */
}
This "nesting" could also use a dot notation, commas, or some other syntax:
/* re-ordering "nested" layers: */
@layer bootstrap.reset {};
@layer bootstrap, base {};
Since layering is determined by order of appearance, we could also support unnamed layers:
@layer url(reset.css);
@layer url(base.css);
@layer {
/* the next layer */
}
@layer {
/* and another */
}
While these layers behave exactly like named layers in every way, they do not provide a "hook" for merging or re-ordering @layer
rules. In most use-cases this would only be syntax-sugar for brevity -- relying on well-organized source-order rather than any explicit names. However, it could be used by teams as a way to "force" an organizing convention (all layer code must be defined in one place), or by libraries wanting to merge & hid" a set of internal "private" layers that they don't want exposed to author manipulation:
/* bootstrap-base.css */
/* unnamed wrapper layers around each sub-file */
@layer url(base-forms.css);
@layer url(base-links.css);
@layer url(base-headings.css);
/* bootstrap.css */
/* the intrnal names are hidden from access, subsumed in "base" */
@layer base url(bootstrap-base.css);
/* author.css */
/* author has access to bootstrap.base layer, but not into unnamed layers */
@layer bootstrap url(bootstrap.css);
What is the migration path for Cascade Layers? #4985
Since this proposal defines Cascade Layers directly above Specificity in the cascade, but below inline style attributes, it should be possible to polyfill the entire feature using ID tags to boost specificity. Most simply stated:
#reset a { /* reset layer */ }
#base#base a { /* base layer */ }
#components#components#components a { /* component layer */ }
In order to avoid requiring a match with ID attributes in the HTML, that specificity can be generated using the :is()
pseudo-class:
:is(#r, a) { /* reset layer */ }
:is(#b#b, a) { /* base layer */ }
:is(#c#c#c, a) { /* component layer */ }
The reality will require some additional finesse to avoid conflicts with existing ID selectors, but those details can be left to individual polyfills.
We believe this proposal addresses all of the use cases discussed, with a few caveats around refactoring and overriding legacy styles. It would be nice for site maintenance to slot all the existing styles into a "legacy" layer, and then have any/all new styles in a higher priority layer. As new styles are added, they should always override the original legacy code. But there are some issues:
- In the normal author origin, unlayered styles will always take precedence over layered styles. Legacy code would need to be given an explicit layer -- but that can be done using the layer import syntax. This workaround is very manageable, but:
- In the
!important
author origin, layers are reversed. If legacy styles are given the lowest normal layering, any legacy!important
styles will jump to the top layer, and become much harder to override.
This would require changes to any legacy uses of !important
-- either manually, or using some kind of css transpiler -- moving normal rules in one layer, and !important
rules into a different layer. Then both legacy layers can be individually placed at the bottom of their respective stacks.
This is a purely mechanical transformation that can be done server side, but if it needs to get syntax-error handling right, it may not be trivial to write.
- Does the loading order of style sheets matter? No, this is the same as existing source-order
- Can user/UA stylesheets also use layers? Yes, this would be available in all origins
- Can you nest layer blocks? Yes, sames as existing at-rules
- Specificity is unchanged inside layers? Yes
- If a stylesheet is loaded in multiple layers, is it both places? Yes, as with current duplicate imports
Needs Discussion:
- What is the best syntax for layer imports?
- What is the best syntax for nested layer names?
- Are un-named layers a feature or a problem?
- Do we need a way to
revert
layers as well as origins? - Does light-DOM layer-ordering impact shadow-DOM layers with the same names?
With this change, browser addons may become unable to override inline styles by using the
!important
forcing a use of harder to develop strategies. Did you take that into account?