Skip to content

Instantly share code, notes, and snippets.

@mirisuzanne
Last active May 13, 2021 06:38
Show Gist options
  • Save mirisuzanne/4224caca74a0d4be33a2b565df34b9e7 to your computer and use it in GitHub Desktop.
Save mirisuzanne/4224caca74a0d4be33a2b565df34b9e7 to your computer and use it in GitHub Desktop.

Cascade Layers, a Proposal

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

  1. Origins & Importance
  2. Context (eg Shadow DOM)
  3. Layers
  4. Specificity
  5. 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:

  1. Transitions
  2. ❗️User Agent
  3. ❗️User
  4. ❗️Author
  5. Animations
  6. Author
  7. User
  8. 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.

  1. ❗️Author
    1. ❗️Reset
    2. ❗️Base
    3. ❗️Patterns
    4. ❗️Components
    5. ❗️unlayered
  2. Animations
  3. Author
    1. unlayered
    2. Components
    3. Patterns
    4. Base
    5. 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.

Style Attribute?

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:

  1. ❗️Author
    1. ❗️Style Attribute
    2. ❗️Defined Layers...
    3. ❗️unlayered
  2. Animations
  3. Author
    1. Style Attribute
    2. unlayered
    3. 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.

Block 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.

Ordering layers:

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.

Import Syntax:

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?

"Nested" Layers:

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:

  1. Author
    1. unlayered
    2. Bootstrap
      1. unlayered
      2. Components
      3. Base
      4. 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:

  1. Author
    1. unlayered
    2. Bootstrap unlayered
    3. Bootstrap Components
    4. Bootstrap Base
    5. 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 {};

Un-Named Layers:

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.

Refactor Use-Case:

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:

  1. 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:
  2. 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.

Questions:

  • 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?
@chrishtr
Copy link

What if a site wants to delay-load the stylesheet for a component library, and respect that library's base vs custom style layers. e.g.

<link rel=stylesheet src=bootstrap.css>
<style>
@layer customize {
.bootstrap-component {
  background: green;
}
</style>

bootstrap.css:
@layer bootstrap-base {
.bootstrap-component {
  background: red
}

If bootstrap.css loads after the inline style tag, then the component will render red, not green, which is not desired.

With this proposal, the only way to fix it would be that the developer has to pre-declare all layers:

@layer bootstrap-base {}
@layer customize {}

Developers may forget to do this, and it also keeps bootstrap from adding layers without risking them breaking on sites. Further, it is a bit of a software engineering abstraction violation that the site author needs to know about all the layers of a component. (If there were an @layer syntax on <link> elements, I suppose it could be solved also by placing a layer name on the stylesheet being loaded.)

For these reasons, it doesn't seem great to me that layer order is determined by stylesheet loading order. I think @lilles was implicitly making this point in a comment above as well.

This could also be resolved in common cases by predefining certain layers and encouraging a best practice of using those layer names when possible.

@chrishtr
Copy link

What happens if the same stylesheet is loaded via multiple layers? Presumably this would mean the stylesheet is represented twice.

@chrishtr
Copy link

This proposal also has the property that the layer/specificity of a style rule depends on not just the order of loading, but the method and nesting of loading. Example:

a.css:
@layer url(b.css)

b.css:
@layer url(c.css)

c.css:
.foo {
  background: green;
}

In this example, the .foo style rule has a double-anonymous layer name for it, and one whose ordering relative to other layers depends on when it was loaded relative to other layers defined outside of c.css. In other words, for a developer to understand how .foo applies, they will have to know about non-local details of how that style sheet is loaded.

It would be better if all of the information they needed was in the same block. That is, @layer { .foo { ...} } is much easier to understand and debug than action-at-a-distance @layer url(c.css).

@chrishtr
Copy link

Here's another bad race condition that can happen:

a.css:
@layer url(b.css)
@layer(d.css)

b.css:
@layer url(c.css)

The order of loading off the network could be a, b d, c. This sounds like it will be confusing to developers, who might otherwise intuitively expect "depth-first" layer ordering.

@lilles
Copy link

lilles commented Aug 14, 2020

For these reasons, it doesn't seem great to me that layer order is determined by stylesheet loading order. I think @lilles was implicitly making this point in a comment above as well.

The order of the stylesheets does not rely on order of loading finished, but the dom and rule order within a stylesheet. But there is a concern around flipping order when non-render-blocking stylesheets finish loading. It's probably a good idea for authors to make sure sheets which are expected to define the order to come early and be render-blocking.

One concern is when you mix components which have different layer stacks, how they would be interleaved and to what extent the page author needs to be aware of layers in imported components. Name clashes could be an issue.

@brunoais
Copy link

brunoais commented Aug 14, 2020

Isn't it the order in the DOM and then the stylesheet instead?
Just like now, if you us js to add a stylesheet to the <head> at the beginning, the results are the same as if it had been there all along.

@vflash
Copy link

vflash commented Jan 27, 2021

You destroy specificity. You are solving a problem that does not exist.
A common problem in development is "specificity". It is difficult to keep it at the same level throughout life.
My opinion needs a tool to explicitly declare "specificity"
For example:

<div class="ui-component my-text">...</div>

:specificity(0).ui-component:not(:first-child) {color: #000}
:specificity(1).ui-component:not(:first-child):hover {color: #f00}
...
:specificity(0).my-text {color: #666}

or

<div class="ui-component my-text">...</div>

.ui-component:not(:first-child) {color: #000; specificity: 0;}
.ui-component:not(:first-child):hover {color: #f00; specificity: 1;}
...
.my-text {color: #666; specificity: 0;}

@trusktr
Copy link

trusktr commented Mar 4, 2021

My opinion needs a tool to explicitly declare "specificity"

👍

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