w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 661 forks source link

[css-cascade-5] Allow authors to explicitly place unlayered styles in the cascade layer order #6323

Open mirisuzanne opened 3 years ago

mirisuzanne commented 3 years ago

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:

Alohci commented 3 years ago

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;

fantasai commented 3 years ago

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;
mirisuzanne commented 3 years ago

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?

Alohci commented 3 years ago

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.

css-meeting-bot commented 3 years ago

The CSS Working Group just discussed Allow authors to explicitly place unlayered styles in the cascade layer order, and agreed to the following:

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
frivoal commented 3 years ago

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.

mirisuzanne commented 3 years ago

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?

FremyCompany commented 3 years ago

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

FremyCompany commented 3 years ago

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.

fantasai commented 3 years ago

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

mirisuzanne commented 3 years ago

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 …?

FremyCompany commented 3 years ago

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)

tabatkins commented 3 years ago

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

FremyCompany commented 3 years ago

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?

Alohci commented 3 years ago

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.

tabatkins commented 3 years ago

No, the point is that, because the initial layer is explicitly named, it's not implicitly removed from the layer stack.

Alohci commented 3 years ago

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?

tabatkins commented 3 years ago

I suppose that would be the implication, sure?

FremyCompany commented 3 years ago

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.

mirisuzanne commented 3 years ago

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…)

mirisuzanne commented 3 years ago

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.

FremyCompany commented 3 years ago

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)

mirisuzanne commented 3 years ago

@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;
tabatkins commented 3 years ago

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

mirisuzanne commented 3 years ago

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

tabatkins commented 3 years ago

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

mirisuzanne commented 3 years ago

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; }
  } 
}
tabatkins commented 3 years ago

All right, yes, I'm in favor of that. Simple and understandable, +1.

fantasai commented 3 years ago

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.

tabatkins commented 3 years ago

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.

mirisuzanne commented 3 years ago

Agenda+ to see if we can get a resolution on this. I think it's the final blocker keeping browsers from shipping this feature.

The more I look at it, the more convinced I am about taking the simple approach here: with a named initial layer that can be ordered like any other layer. We always want to give the final site author control over the final layer order, and any attempt to have layers declare their own position relative to the default goes against that goal. Frameworks should not be able to place themselves above or below the initial layer. The final author should make that decision by establishing layer order up front.

jensimmons commented 2 years ago

I lean towards keeping this simple. The up down thing feels over engineered to me.

I also don't like the word initial. It may make sense to folks on the CSSWG, but to the outside world of web developers, initial means browser defaults. Their unlayered styles are not the initial styles. The UA styles are the initial styles. I confirmed this with a name-bikeshed bait tweet.

I believe unlayered or some other word that means “outside a layer” would be better. By using the word “layer”, we make it easier for non-English speakers to pair the two words together: @layer and unlayered or (similar).

fantasai commented 2 years ago

@jensimmons Wrt syntax, I'm quite open to other keywords. We could alternatively use a slash:

/* layered below unlayered (default) */
@layer foo;
/* foo, bar, unlayered, baz */
@layer foo, bar / baz; 
/* bar above unlayered */
@layer / bar;

The thing that's important to me is that when you import a stylesheet using @import or import it using a server-side include, it doesn't unexpectedly re-interpret the cascade order of things within your style sheet.

mirisuzanne commented 2 years ago

If we do go with a two-list option (above and below unlayered styles), I like the idea of doing it with a slash instead of a keyword.

css-meeting-bot commented 2 years ago

The CSS Working Group just discussed explicitly place unlayered styles into the layer order, and agreed to the following:

The full IRC log of that discussion <TabAtkins> Topic: explicitly place unlayered styles into the layer order
<TabAtkins> github: https://github.com/w3c/csswg-drafts/issues/6323
<TabAtkins> miriam: We left this at taking back to the thread to discuss syntax, and a lot happened
<TabAtkins> miriam: two frontrunner proposals
<TabAtkins> miriam: (1) unlayered styles are like any other layer, we just have a name for that layer and allow you to place it
<TabAtkins> miriam: That's the simplest option conceptually
<TabAtkins> miriam: it means the final page author always has control over layering, as they can place the unlayered style wherever they want
<TabAtkins> miriam: But it could cause problems if people explicitly need their styles above or below the default
<TabAtkins> miriam: If we need that...
<TabAtkins> miriam: (2) track two layer stacks, one above and one below the default unlayered styles
<TabAtkins> miriam: if we need that, fantasai and i came up with a syntax, you can put a slash in your @layer to designate whether your layers are in the "below default" or "above default" stack
<TabAtkins> miriam: think this'll work well, but you're conceptually tracking two layer stacks
<TabAtkins> fantasai: Third option is close no change, and just leave unlayered at the top of the layer stack
<Rossen_> q?
<TabAtkins> jensimmons: I talked with elika about this a lot recently. i think i agree with elika that the simpler version won't work.
<TabAtkins> jensimmons: Think about a team with a complex custom design with their own styles, then they pull in bootstrap or other third party complicated lib
<TabAtkins> jensimmons: And you crush those two together, there's a lot of room for unintented consequences
<TabAtkins> jensimmons: But as we talk thru the more complex versions, it's too complex
<TabAtkins> jensimmons: layers are meant to make the cascade more understandable, and i think making these two stacks, plus the combo with important vs normal, i think it'll be too hard
<TabAtkins> jensimmons: and there is a way - i'd need a diagram to fully explain - for the 3rd party framework to say "hey I want to rearrange the order of the layers" and end up overriding layers the custom stack doesn't want overridden
<TabAtkins> jensimmons: As soon as I started to understand how this could happen, I just said NOPE
<TabAtkins> jensimmons: So right now I'm okay with option 3, just leave the unlayered styles at a specified point. We can revisit in the future if we think we really need it.
<fantasai> TabAtkins: I was not a fan of the 2-stack approach
<fantasai> TabAtkins: while there are use cases for control, I'm fine with leaving no change for now
<fantasai> miriam: wfm
<TabAtkins> Rossen_: anyone object?
<TabAtkins> RESOLVED: Reject this proposal; unlayered styles have a specified location in the layer stack which can't (currently) be controlled
bramus commented 2 years ago

With apologies for blowing new life into this issue, but after getting several questions about this (in replies on posts/tweets or during a Q&A after a talk) I feel that people need a solution to this problem.

To summarize the problem: unlayered styles win from layered styles and that is …

  1. … great when working with frameworks and 3rd-party CSS: put those external things in layers, and you can be sure your own (unlayered) rules win from those layered ones. Yay! 🎉
  2. … not so great when doing the Progressive Enhancement dance. Browsers that don't understand @​layer will ignore the layered styles. This way you end up with a bunch of layered reset/3rd-party styles not being loaded at all in those browsers. Author unlayered styles then get applied on top of nothing, which is like building the tenth floor of a flat but with the foundations and nine floors below it missing. Weird. 😩

Getting people sold on that 2nd item is very hard (and until now impossible) sell to be honest. My message right now is that while layers are great, you can't really use them in a PE-mindset:

This is a message I don't really like to bring?


Summarizing this issue, I saw syntax proposals to:

  1. Define the position of layered styles using an additional keyword (up and down where proposed) or using a /
  2. Position the unlayered styles by naming them (Using names such as initial, unlayered, etc.)

I would like to request the CSS WG to look into these syntax suggestions and to provide a structural solution. That way we can have both Layers and keep Progressive Enhancement in mind while building websites.

FremyCompany commented 2 years ago

Hi @bramus,

Having layered styles always win is not going to make your site work in browsers that don't support layer if you don't do anything, I fail to see how this would be of any use at all to fix the Progressive Enhancement problem. All styles must work for a site to work, not just some.

There is no other solution, people who want to use @layer today can, as long as they use a (compile-time) polyfill.

Here is the conversion that is required:


  /* this style will win even if this is the first h1 because it's unlayered */
  h1.main { color: green; }

  /* these styles are like defaults */
  @layer x {
      article > h1:first-child { color: red; }
      h1 { color: blue; }
  }

=>


/* layer styles must come first, with :where so they have no specificity and get overriden by all other styles */
/* this means that the rules have to be sorted by specificity per layer, otherwise this won't work */

  /* these styles are like defaults */
  :where(h1) { color: blue; }
  :where(article > h1:first-child) { color: red; }

/* unlayered styles can come in regular order, after the unlayered styles */

  /* this style will win even if this is the first h1 because it's unlayered */
  h1.main { color: green; }
tabatkins commented 2 years ago

I agree with @FremyCompany; changing the order won't help you much, if at all, when @layer rules are discarded in legacy browsers. This is something that should be addressed with a CSS compiler, if that's a need, and luckily the feature that makes it reasonable to do so (:where()) is older than layers (but you can do it even without :where(), it just requires even more work from the compiler).

bramus commented 2 years ago

What I had in mind was the inverse of current promoted approaches, namely: keep the resets and base styles unlayered, and instead layer up the rest of the author styles (giving them a higher priority than the unlayered styles).

That way:

This approach wouldn't require any jumping through hoops by authors, nor the use of a build step. It just works!

With this, an author can also decide for themselves whether they want to layer 3rd-party styles or not. Should the specificity of the 3rd-party CSS conflict with their own styles, then it would be recommended to do so.

tabatkins commented 2 years ago

Right, @FremyCompany's point is that this wouldn't, in any meaningful sense, "just work". You're still missing all your layered styles.

It seems like you're operating on the theory that this is similar to JS and "progressive enhancement", where you can design a basic experience using older tech (like writing an app using <a>s and several distinct pages) and then layer new fancy tech on top for a better experience in new browsers (like using fetch() to turn it into a single-page app). That's not the case here; layers aren't cool new tech, they're organizational. The stuff you put in layers is identical to the stuff not in layers; there's no dividing line to draw in terms of "enhancement".

This is much more similar to switching to JS arrow functions over the old function declarations; the code is identical in behavior either way, it's just more convenient to write with the new stuff. You wouldn't separate out your code into "basic" stuff using function and "new, better" stuff using arrow funcs that's okay to be skipped in older browsers. You'd either use function everywhere, or use arrow funcs and a JS compiler to turn it into old-browser-compatible code, or just wait until arrow funcs are widely supported and ignore the minority of older browsers.

bramus commented 2 years ago

This wouldn't, in any meaningful sense, "just work". You're still missing all your layered styles.

In some cases missing the layered styles could still give you an acceptable result. I don't wholeheartedly agree with the “All styles must work for a site to work, not just some.” statement as a site can work just fine without any styles at all.

But OTOH, yes, I can also see that in other (many? most?) cases it would be feasible to have all styles loaded.

This is much more similar to switching to JS arrow functions over the old function declarations […]

I see. Reasonable explanation. Thanks, for enlightening me on it.

Layers aren't cool new tech

Harsh! 😛🙃

mirisuzanne commented 2 years ago

Yeah, @bramus your argument is exactly the one that took us back and forth on this a few times - but I think Tab is right that once we accept there's no dividing line to draw in terms of "enhancement", then there are several advantages to making it so 1) the behavior is simple and consistent for everyone 2) a primary use-case is lowering priority of tools.

I also like that the result of that decision (layers decrease importance) acts to push against the common assumption that this will escalate things by making everything more and more important.

In any case, we need a good polyfill to offer people. OddBird is starting to look into this, but it's not very far along yet. Always open to contributions (or alternative approaches).

yisibl commented 2 years ago

Sorry, the discussion here is a bit long and I didn't understand it.

Is it legal to use CSS-wide keywords in <layer-name> or not?

e.g.

@import 'foo.css' layer(inherit);
@layer unset {}
@layer revert-layer {}
mirisuzanne commented 2 years ago

@yisibl I don't think that was part of this issue, but no – CSS-wide keywords are not allowed in layer names: https://drafts.csswg.org/css-cascade-5/#typedef-layer-name

jeremyredhead commented 1 year ago

Right, @FremyCompany's point is that this wouldn't, in any meaningful sense, "just work". You're still missing all your layered styles.

If unlayered rules came first instead of last, then I don't see why you couldn't just use old-fashioned methods to construct a basic reset+layout style that is just barely good enough, then (lazily :P) use the new hotness (layers) to do themes & everything else.

Sure, it would be highly unorthodox. But well within the realms of possibility (unless I am missing something).

Just as you could choose to define some fundamental/barebones functions in one JS file, then in another file using newer syntax, override those barebone functions and/or define "extra" functions that are only called if defined.

Granted, I can't think of a single reason why you'd ever want to do this with JS. But the CSS example is salient & tempting.

kizu commented 1 year ago

Should we maybe reopen this issue? Or should we open a new one?

From what I understand, the initial issue was the one preventing the browsers from shipping this as a feature. Now that the layers have shipped, it is obvious that a bunch of use cases are not covered by them.

Both the Candidate Recommendation and the latest Editor's Draft list this as an issue, highlighted in red and linked there. But the issue itself is closed.

The initial WG resolution is this:

RESOLVED: Reject this proposal; unlayered styles have a specified location in the layer stack which can't (currently) be controlled

Note the “(currently)”. I would interpret this as “ok for the first implementation, which we could return later to”. And this was actually the way I did interpret this in the past, and usually looking in the specs, seeing the still-present issue mentioned, I thought that this was a still-open issue.

I did bring this issue up recently on Mastodon, where it got some support.

The main use cases that interest me personally are style overrides for any sites or apps from outside:

One of the best things of CSS, its first letter — the Cascade — always had this intent of treating the users as those who should be able to write CSS as a way to customize websites. Not allowing the users to have a simple way to override things goes against this intent. While it can be argued that maybe browsers themselves should provide convenient ways of writing user origin styles, in reality they don't. Layers seem like the perfect place to unlock the overridability of the web, and make it more accessible for everyone.

I acknowledge that it is not as easy to decide how exactly the ability to add layers that go above unlayered styles should be defined, but I strongly think that we require this ability.

mirisuzanne commented 1 year ago

I don't think the problem here was a lack of use-cases, but the fact that we couldn't find a good path forward. I'd be happy to have this open again for more proposals, but I don't think it's a simple problem with a clear solution.

I'd also point out that user styles exist, and browsers have systematically made them harder to access. It sure seems like the problem of user-overrides should be solved in the 'origins' part of the cascade, rather than the 'layers' part? I don't mean to say that technical purity should block a more practical solution, if layers really are the best path forward - but it sure seems like we should look at the user origin first?

kizu commented 1 year ago

but it sure seems like we should look at the user origin first?

I don't know how CSSWG could impact this: to me this seems like a browsers' UI & UX issue in the first place. The ability to have user origin styles is already specified, the issue is that it is underused. And I'm not even sure if browser extensions can provide user origin styles, actually? I'm not knowledgable into how the whole extensions API is used, if it standartized and if it is even possible to require browser extensions to be able to provide user origin styles.

Also, all of this only covers one of the cases: the per-user in-browser overrides, while cases like “we'd want to provide styles for a mastodon instance as a whole as overrides, but not fight with the existing styles' specificity” won't be covered, as this is much closer to the layers idea.

mirisuzanne commented 1 year ago

I don't know how CSSWG could impact this

On the one hand CSSWG can't tell browsers what features to implement. On the other hand CSSWG is mostly browser vendors. Maybe more to the point, user styles are (currently) designed for a more 'preference'-like use-case, which browsers do support through limited GUI forms. To do real custom overrides from the user origin, you'd be back in the world of !important. Or we'd need some other extension to user origins, which browsers and users find compelling. I imagine that may be why customization extensions don't bother with the user origin, though maybe they also lack access.

Also, all of this only covers one of the cases

Yep. Totally.


But to back up through some of the previous discussion here, and maybe reframe the feature request…

Assigning unlayered styles a universal-but-custom priority is what causes many of the issues, because 'unlayered' is a shared default. Usually, each stylesheet can namespace its own layers, and avoid conflicts when needed. But you can't name a layer that isn't there, and you shouldn't be moving around a layer that every other stylesheet treats as a default. Change its position for one stylesheet, and it breaks for others.

If every stylesheet has the same layer, it's the default layer, and anyone can move it around…? That gets pretty confusing and unreliable.

But the ability to layer a stylesheet is localized, it doesn't impact other unlayered styles, and it doesn't even impact the relation between layered and unlayered styles inside that stylesheet. Stylesheets remain internally consistent, even when layered on import. So it's already possible to position unlayered styles in the cascade layer order, right? The solution to unlayered styles might just be… layering those styles?

For the individual user cases, I think that should be possible for JS to do on the client side? Which an extension would be perfect for, since it can guarantee layer support.

And applications like mastodon and wordpress can start layering their own styles, if they want to expose customization features to their users? I hope they do! I know Wordpress has active discussions about it.

So then the question might be: can I as a masto admin override what Mastodon provides as part of their app? Which… I'm not sure if that makes sense as a CSS feature? Isn't that a feature request on the applications themselves?

bramus commented 1 year ago

# The solution to unlayered styles might just be… layering those styles?

Yes, but if you old browser doesn’t understand layers, you’re screwed and you end up with no styles at all.

# Can I as a masto admin override what Mastodon provides as part of their app? Which… I'm not sure if that makes sense as a CSS feature? Isn't that a feature request on the applications themselves?

Problem is when your application – be it Masto, WordPress, or whatever – does not use layers, you are prevented from using layers at all to add some extra overrides. You must use unlayered styles from that point on.


Personally, what I’d love to see is a simple way to say “this is a layer, but it wins from unlayered styles. No need to reorder any existing ones or finely control where the unlayered styles should go.

In some thread about @scope I floated around the idea of adding a ! to the at-rule itself, to indicate that it’s a strong one. For layers, this would be @layer!. These would stack on top of unlayered styles, using the same layer logic for determining the order and what not.

E.g.

@layer! a, b, c;
@layer d, e, f;

@layer! g { … }
@layer h { … }

… would leave you with this order (ranked from high priority to low priority): g, c, b, a, unlayered, h, f, e, d.