[css-cascade-5] Allow authors to explicitly place unlayered styles in the cascade layer order
Regarding CSS Cascade 5 (cascade layers), @jensimmons commented on another thread about layer ordering:
Would it be possible to allow Authors to set for themselves where in the cascade the "unlayered layer" resides? Maybe they even want to sandwich it in-between. A mechanism that's part of however they define the named layers & determine which layer is "first" & "second", etc.
~~By default unlayered style come first (lowest cascade priority) in the source order~~, but this would allow more explicit placement. Roughly (pseudo-code):
EDIT: That's no longer the case. In https://github.com/w3c/csswg-drafts/issues/6284 we reversed the behavior, and now unlayered styles have the highest priority. This explicit placement would still be useful, since there are use-cases for both approaches.
/* the default behavior */
@layer <unlayered-styles>, reset, framework, components, utilities;
/* placed "in-between" layers */
@layer reset, framework, <unlayered-styles>, components, utilities;
/* placed at the top/end of the layer order */
@layer reset, framework, components, utilities, <unlayered-styles>;
I think that feature makes a lot of sense, and I would likely use it as an author. A few considerations to keep in mind, as we develop a mechanism for this:
- I would expect it to be repeatable for implicit sub-layers, as well the implicit outer layer
- Since all layer names are currently custom-idents, we either need a reserved name, or some way of distinguishing the provided implicit-layer ident from author-provided idents.
Definitely welcome to have that level of control. If you didn't want a reserved name you could use different @ name. e.g. @layer reset, framework; @unlayered; @layer components, utilities;
Idk if it's a good idea, but one possibility would be to just leave out the identifier.
@layer reset, framework;
@layer;
@layer components, utilities;
Thinking about this a bit more: every layer (including but not limited to the default/root layer) has the potential for both direct style-rules and nested sub-layers. So this feature might be useful in nested context, not only in the root/default situation:
@layer one;
@layer;
@layer one {
@layer two;
@layer;
}
On the other hand, the root/default layer is the only place where authors might not be able to add explicit layering – for the sake of backwards compatibility. Once styles are layered, there is no harm in layering them further. So from that perspective, control is only needed for fully-unlayered styles.
Is it confusing if authors can specify different defaults inside each layer context? Is it more confusing if this only works at the top level, and does not work in nested contexts?
It would be a shame in my opinion if it didn't work the same in nested layers.
I'm also not sure how @import url(links.css) layer(mylayer); would work if @layer; didn't work the same when it becomes nested by the import as it does when the import doesn't inject it into a layer.
The CSS Working Group just discussed Allow authors to explicitly place unlayered styles in the cascade layer order, and agreed to the following:
-
RESOLVED: Reserve the CSS wide-keywords (making the whole layer block invalid at parse time) for now and details TBD when we have better use cases
The full IRC log of that discussion
<emilio> topic: Allow authors to explicitly place unlayered styles in the cascade layer order<emilio> github: https://github.com/w3c/csswg-drafts/issues/6323
<emilio> miriam: this one is another coming from an earlier resolution
<emilio> ... we resolved that unlayered styles are lower pri
<jfkthame> present-
<emilio> ... jen asked about whether it'd be useful to tweak the unlayered styles priority
<emilio> ... there's some syntax proposals in the issue
<Rossen_> q?
<emilio> ... and I'd expect it to work at each level of layering
<emilio> ... are we happy with an empty layer rule syntax? Does this become too complex?
<emilio> florian: I could see use cases for top/bottom, has any non-theoretical use case come up for in the middle?
<emilio> miriam: yeah, you want components at the top and resets on the bottom, so you might want most of your styles between them
<emilio> TabAtkins: Like florian I see the use case but I'm not sure we need to solve it right now
<emilio> ... we could resolve the CSS wide keywords as layer names in case we want to solve them
<emilio> miriam: does that become a problem if additional wide-keywords are added?
<Rossen_> ack fantasai
<emilio> TabAtkins: theoretically? But we haven't added many over the years
<TabAtkins> s/resolve/reserve/
<emilio> fantasai: we could also do something that isn't a keyword
<emilio> ... I don't have strong opinion on having to solve this now, and I'd be ok reserving the wide-keywords
<fantasai> s/keyword/keyword, like an asterisk/
<emilio> florian: maybe I need to re-read the minutes for when we decided to switch top/bottom, I wasn't there and it seems !important could take care of jumping to the top
<emilio> miriam: main reason for that was that putting them at the bottom allows progressive enhancement
<emilio> ... sort of like when not all browsers had media queries you'd write the specific styles in there
<emilio> ... but lots of people think of layers as a way to hide their resets
<emilio> florian: I guess I see it more like the later but that also doesn't give me a strong use case for having unlayered styles in the middle
<emilio> ... I'd be fine reserving the wide keywords though
<emilio> fantasai: so there's the question of whether we add it now, if we don't we might want to just reserve the keywords
<emilio> miriam: if we're not sure if it's needed I'd be ok with reserving the keywords and delaying
<emilio> ... since it adds a fair amount of complexity
<emilio> florian: what do we need by reserving the keyword? Just making them syntactically invalid?
<emilio> fantasai: yeah, if you define @layer with that keyword the whole block is in invalid
<emilio> florian: is that progressively-enhanceable? If you add a layer that doesn't work and then it starts working...
<emilio> fantasai: why would you type it in if it doesn't work?
<emilio> florian: would it be wholly invalid or just ignored?
<emilio> TabAtkins: could we bring that detail back to the thread?
<emilio> Emilio: fwiw it seems simpler to make the whole block invalid at parse time
<emilio> RESOLVED: Reserve the CSS wide-keywords (making the whole layer block invalid at parse time) for now and details TBD when we have better use cases
Given the resolution above, I think we all agree that @layer initial { } is simply invalid and the whole block is rejected. However, what of @layer foo, initial, bar;? Is that whole rule rejected too, or do we simply order foo and bar and ignore the non-existing initial layer?
I'm not sure, but I suspect making the whole rule invalid is safer. Otherwise, we might have people who introduce a @layer initial { } block, fail to notice that that doesn't do anything, order that layer into the middle of the stack with @layer foo, initial, bar;, and some day, if we do make the initial keyword apply in that situation, that changes the ordering of their whole page. I think this scenario would be less likely to happen if we ignore the whole rule, as then the author would be confronted with the fact that the ordering of their foo and bar layer don't work either, making it easier to notice.
I'm happy with that approach, and drafted some spec language around it. Not sure if we need to get an official resolution, or not?
I noted while thinking about this in the context of #6284 that explicitly pinning "initial" as a layer name isn't very convenient, because this has to happen once, and so that first declaration needs to be aware of all the layers that need to be above or below the initial layer (which might not be possible if you are using themes / unrelated add-ons).
Another approach I have been thinking about is to have two lists independently, all layers that must be above the unlayered styles, and all who need to be below.
A strawman would be as follow (`!important following an at-layer name means to put in the list after unlayered styles):
@layer reset { article h1 { margin: 0; } }
@layer theme { h1 { margin-top: 1.2em; } }
@layer special-overrides !important { h1:first-child { margin-top: 0 } }
.main-title { margin-top: 0.2em; } }
That would yield the following order for the layers : reset < theme < /initial/ < special-overrides.
Adding !important to individual values has an effect as usual, the !important on @layer only changes the order of layers in the list, but does not propagate to the values themselves (but the change in order for the layer will make the declaration be more important).
The advantage I see is that we are not prescribing anything here about the specifics of the default, authors can get one or the other depending on their needs.
I think it's going to be useful and important to be able to have one-off layer declaration blocks that are above or below the default-layer styles, so we should have a syntax built into the @layer rule that says whether it goes above or below. Something like:
@layer [ up | down ]? <layer-name> { ... }
where
-
upincreases the priority of normal rules (and decreases priority of !important rules) -
downdecreases the priority of normal rules (and increases priority of !important rules)
I like the goal here, but have a few questions.
As I understand this, we would basically be creating two layer stacks — one above and one below the default — and then use the keywords to append layers to the top of either stack? What's the result of these cases?
@layer up one;
@layer down one;
Does that give us two layers with duplicate names (upper one & lower one)? Or do we only allow this keyword when the name is first used (in which case the second rule is invalid)? Another option is that we only provide this one-off syntax for truly one-off unnamed layers? In which case we likely need both the explicit placement, and the one-off option.
@layer <layer-name> | [ up | down ] { ... }
I assume we don't want to allow moving layers around retroactively, so the second rule should not impact the layers defined in the first rule. That's what up/down imply to me. So if that's not what we mean, I think we should name the two layer stacks, and use their names as the keywords: something more like upper/lower or default/important or …?
Given my proposal is to have two independent lists, I guess it's fine to have two layers named "one", one in each list.
Another way of seeing this is that one layer would be named "one" and the other "one!important" without préjudice to each other.
(Or, you know, "up one" and "down one" if we adopt fantasai's syntax)
I'm not a big of this "two lists" idea - it makes ordering less obvious (no longer order-of-appearance, but two simultaneously-calculated orders of appearance), and it brings up identity questions like what @FremyCompany referenced without good answers.
"One-off" layers that are before/after a specific significant layer are already something authors might want for any layer, but we don't allow that because it's better for the page to declare its layer order up-front; it makes the entire feature more understandable and manageable over time. I don't think the default layer is in any different in this respect.
(We do at least let styles arbitrarily inject themselves after a given layer via nesting; an @layer foo.bar{...} comes after all the "foo" layer styles. We could allow nesting under initial (assuming that's what we call the default layer) to let people achieve some of this, in a way that's consistent with any other layer.)
Why not. I would be totally fine with
@layer initial.overrides {
... /*above unstyled*/...
}
But then maybe "initial" is a strange name for that layer. What are our other options? Unset?
I don't follow that.
@layer initial.overrides {... }
is the same as
@layer initial {
@layer overrides { ... }
}
except that
@layer initial { ... }
is implicit, so is the same as
@layer overrides { ... }
which is (now) an underpin (down) layer, not an overrides (up) layer.
No, the point is that, because the initial layer is explicitly named, it's not implicitly removed from the layer stack.
So one could do @import url("tabs.css") layer(initial); and all internal layers defined within tabs.css would then override the unlayered rules in the same file?
I suppose that would be the implication, sure?
Another possibility is to add another layer that is not the unstyled layers but is one just after it. Then confusion drops a lot I guess, especially if we give it a better name.
Something like @layer !important.xyz { ... } where the !important layer is a special layer that is above the unstyled styles.
I like the !important direction - it serves the same purpose as using initial, but adds a bit more clarity about what's happening.
(Except that !important declarations in the !important layer would have lowest priority. Something something two wrongs make a right…)
All these approaches get a bit messy when we put them into a nested context — which @alohci pointed out is un-avoidable. The keyword option only works if we nest entire blocks, but becomes semantically unclear when using the nested name syntax:
@layer up framework {
@layer down defaults { … }
@layer up components { … }
}
/* how does this utilities layer relate to framework.<unlayered>? */
@layer up framework.utilities;
The implicitly named override layer (e.g. !important) can be repeated at each level, but the result is verbose:
@layer !important.framework.!important.utilities;
After a conversation with @fantasai, we're proposing a variation on the original proposal, using the initial keyword in layer list declarations. But rather than placing it once, like other layer names, we would treat it as a relational anchor for the other layers in the list. The following declarations can be folded together:
/* each use of initial is scoped to that layer rule */
@layer reset, initial, utilities;
@layer defaults, initial, overrides;
/* the result maintains relations to initial */
@layer reset, defaults, initial, utilities, overrides;
There is still a single layer stack (and shared namespace) for layer names at any level, and layers still default to placement before/under the initial layer, unless placed after initial in a list. Authors could also use a convention to achieve more explicit under/over layer names, if desired:
/* can be defined at any level of nesting */
@layer down, initial, up;
/* or even described in the nested syntax */
@layer up.framework.initial, up.framework.up.utilities;
There is still potential for name collisions that change the intended layer order, but those collisions are limited to author-defined layer names (which can already collide). In this case we would want to follow established conventions for name collision — such that the first mention takes precedence. For example:
@layer initial, framework;
@layer framework, initial;
Results in a single framework layer above the initial unlayered styles. The inverse can also be resolved using our existing rules. Any of these options result in framework being placed below initial, since the first mention of framework has it placed either explicitly or implicitly below:
/* single list */
@layer framework, initial, framework;
/* split v1 */
@layer framework;
@layer initial, framework;
/* split v2 */
@layer framework;
@layer initial, framework;
Note: Given that approach, the following are meaningless, and should be treated as invalid:
/* nothing is being anchored */
@layer initial;
/* can't have two anchors in one statement */
@layer initial, framework, initial;
/* initial is not a layer that accepts sub-layers */
@layer initial.resets;
We think the combination of a list-anchoring syntax, combined with the option to create more explicit conventions, should cover the majority of use-cases — but we'd love to get more feedback here. Especially from authors.
Sounds doable as well. But this begs the question: why make initial the only layer you can order relative to?
@layer base, theme, initial, overrides;
...
@layer base, base-overrides;
(with order base, base-overrides, theme, initial, overrides)
@FremyCompany If you applied our proposal to base in this case, it would actually result in an order of base, theme, initial, overrides, base-overrides. We're not anchoring directly next to the initial layer, but on a given side of it. Otherwise the names are still stacking in the order they are encountered.
But your outcome can already be achieved with nesting:
@layer base, theme, initial, overrides;
...
@layer base.initial, base.overrides;
Okay, so initial wouldn't be a "layer", per se, just an indicator of whether a given (top-level?) layer is in the "before unlayered" or "after unlayered" lists.
I still don't think I'm a fan of this. It still presents what I consider a confusing situation, where the layers are not in order-of-appearance, but rather maintain two completely separate orders of appearance. I continue to question why we want to allow this, when multiple @layer statements aren't a great idea anyway, since they can't control their interweaving. It's a good practice to declare all your layers (the top-level ones, at least) in a single @layer statement.
Like, I think this:
/* each use of initial is scoped to that layer rule */
@layer reset, initial, utilities;
@layer defaults, initial, overrides;
/* the result maintains relations to initial */
@layer reset, defaults, initial, utilities, overrides;
is a confusing result, and (rightly!) not something that can be achieved relative to any other layer.
I still think it's best to go with with the simplest possible solution: initial is a layer name that refers to the unlayered styles, and is reserved to only be usable at the top level. Just like any other layer name, it takes its position from its first mention; if not mentioned, it goes first or last (I forget which way we've settled on). Anything else is adding functionality to the initial layer that no other layer has access to, nor do we want to give them access to, and I don't understand why the initial layer is special enough to need that.
(As I said in an earlier comment, we could still allow the initial layer to have nested layers like a normal layer, but we don't need to.)
I agree that we should keep things as simple as we can. And if we can avoid too much special-casing, that's great. Yes, best practice is likely to be ordering as much as possible up-front.
But we've explicitly said that one of the goals is making it easier to consume third-party styles, and slot them in. That means we will often be dealing with stylesheets that have different assumptions about internal layer ordering. So far that's ok, because you can consume an external file while also nesting it inside a layer. For that to continue working, we have to handle the situation where framework.css is designed with unlayered styles in the middle, but is then nested into the specific layer of a site's styles.
So I'd be ok with "initial is just a reserved layer name" – but not if that behavior is restricted to the root layer. Allowing it to be nested is the only way to make it work along with the existing logic. After a conversation with @jensimmons, she also liked that approach, but proposed unlayered as a more clear keyword.
(I understand the reason people have been pushing for additional special-casing – but I think "nesting namespaces" has been our proposed solution for other potential naming conflicts, and I believe we can apply it here as well.)
So I'd be ok with "initial is just a reserved layer name" – but not if that behavior is restricted to the root layer. Allowing it to be nested is the only way to make it work along with the existing logic.
Does this mean that when nested, initial refers to the styles in that layer that aren't further nested into sub-layers? Or something else? (An example would probably help.)
Yes, it would refer to styles that are not further layered. Given the following, and an element with class='btn green', the default will be maroon (at each level, initial styles take precedence):
.btn { background: maroon; }
@layer framework {
.btn { background: rebeccapurple; }
@layer utility {
.green { background: green; }
}
}
But if we want framework.utility to take precedence, we can do it by re-ordering initial layers. Either once up front:
@layer initial, framework.initial, framework.utility;
Or defining the order for each level of nesting:
@layer initial, framework;
.btn { background: maroon; }
@layer framework {
@layer initial, utility;
.btn { background: rebeccapurple; }
@layer utility {
.green { background: green; }
}
}
All right, yes, I'm in favor of that. Simple and understandable, +1.
My concern here is that a layer (whether named or anonymous) that is not accounted for in the first statement that mentions initial has no way to pull itself ahead of the unlayered rules. We decided to have a default behavior, and that's fine, but you can't actually change the default behavior without naming the layer and listing it at the top of the file. We decided not to require all layers to be named up front, so I think wanting a different default behavior for layer precedence shouldn't impose that requirement.
Also, in every other case, if you take a bunch of separate CSS files containing @layer rules and combine them into a single file, they behave the same way as if you imported them in that order, as long as you avoid naming clashes. But initial is guaranteed to name-clash, so treating it as just a regular layer with an automatic name means it's effectively ignored in later rules, and that breaks this invariant.
Nesting is not a solution to this problem because you might want all of the unlayered styles to cascade together. Nesting is not merely a namespacing mechanism, it changes the cascade also.
My concern here is that a layer (whether named or anonymous) that is not accounted for in the first statement that mentions initial has no way to pull itself ahead of the unlayered rules.
Yes, but that's true of every other layer, too. If you have a foo layer early in the order, a later bar layer is unable to ever put itself before it unless it's explicitly named as such in a preceding @layer bar, foo rule. What's special about the initial layer such that it requires us to favor it here?
Remember that the intention is that if you're using layers, ideally all your styles are layered so you have full control over the ordering; we need to handle the initial layer just to help with adapting an existing unlayered codebase into using layers. It's not meant to be special in any particular way otherwise.