w3c / csswg-drafts

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

[css-cascade] Proposal: `@layer initial` should always be the first layer #10094

Open mayank99 opened 7 months ago

mayank99 commented 7 months ago

Problem

Currently, layers all need to be explicitly defined. This means the page author needs to be very disciplined in setting their layer order upfront, before adding any “real” styles.

This almost always involves creating a layer named something like “defaults” early on.

<head>
  <style>
    @layer defaults, components, utilities;
  </style>
  <link rel=“stylesheet” href=“styles.css” />
</head>

This works, but is problematic for a few reasons:

Proposal

Allow authors to declare all low-priority styles in a layer named initial.

@layer initial {
  *, ::before, ::after {
    box-sizing: border-box;
    margin: 0;
  }
} 

initial is one of the layer names that are "reserved for future use", so this layer can be automatically set up by the browser, such that:

  1. It is always the first layer that comes before all other layers.
    • It is always the first within a context (e.g. shadow context vs host context).
  2. It always exists automatically, i.e. it doesn’t need to be explicitly created.
    • This means @layer initial exists implicitly, regardless of the page’s layer order, similar to the implicit “outer” (unlayered) layer.
@layer A, B;

@layer initial {
  /* this will cascade before A and B */
}

Polyfill

In my testing, I found that the three major browsers do not do anything special to the “reserved” layer names, even though the spec says these must be invalid at parse time. I initially thought this was a bug and/or maybe the spec should be changed (see #10067), but then I realized that the current browser behavior makes this feature somewhat easy to polyfill, by setting @layer initial; as the very first style. This could even be done automatically by frameworks.

<head>
  <style>@layer initial;</style>
  …
</head>

Use cases

This solves all of the problems described above, and makes cascade layers easier to incorporate into existing workflows.

Open questions

  1. Should initial also be implicitly set up for nested sub-layers? (I think yes)

    @layer foo {
      @layer A, B;
    
      @layer initial {
        /* comes before foo.A and foo.B */
      }
    }
  2. Should authors be allowed to add sublayers into initial?
    @layer initial.foo { … }

    This one is interesting. I think it would be useful ("lowest of the lowest priority") and it would match how the outer implicit layer contains author-defined explicit layers.

tabatkins commented 7 months ago

In general, "special syntax to make something go first" isn't very composable. Things end up fighting over being first, the set of things putting themselves first still needs to establish a relative ordering. You just kick the bucket of "it's hard to coordinate" a little bit down the line, but not very far.

On the other hand, we have had multiple proposals for something like !default as the inverse of !important, just a way to downgrade a style to be weaker than all "normal" styles.

So this might indeed be reasonable, just establishing that there's a specific layer that's always defined and before all named layers. Some specific comments:

mayank99 commented 7 months ago

I don't think named layers need an "initial" sublayer. By definition, you're in control of the named layer; you can set up your sublayer order immediately. The only possible concern is that someone else is using the same layer name and so you're fighting over the namespace, but that just means you should use a different name.

It can be useful when the top layer is set up by someone else, such as when a stylesheet is imported into a layer. It would be confusing if the behavior of initial changes depending on how the CSS is imported.

// In foo.css
@layer A, B;

@layer A {…}
@layer B {…}

@layer initial {
  /* expected to go before A and B, normally */
}
@import "foo.css" layer(foo);

/* foo.initial now comes after foo.A and foo.B 🙁 */

Of course there is a workaround, so maybe not a big concern.

@layer initial, A, B;
mayank99 commented 4 months ago

I was thinking about this again, and I'm starting to wonder if we need something like @context to allow authors to place styles before or after the document and shadow contexts.

Let's think about CSS resets again. Most sites do something like this today:

*, ::before, ::after {
  box-sizing: border-box;
  margin: 0;
}

This has low specificity and can be de-prioritized further using @layer. But it still takes precedence over the previous contexts. This is a problem because it makes it hard to write styles using :host and :slotted selectors.

<my-component>
  <template shadowrootmode="open">
    <style>
      /* This gets overridden by our reset 🙁 */
      :host { margin: 1rem }
    </style>
    <slot></slot>
  </template>
</my-component>

What if we instead allowed authors to put styles in a context that precedes all other contexts? This would be the ideal place to put resets and default styles.

@context defaults {
  *, ::before, ::after {
    box-sizing: border-box;
    margin: 0;
  }
}

To take this one step further, it would be equally useful to have a context that comes after all other contexts. This would be a great place for e.g. browser extensions to put user styles (see #7535/#6323). For this to work, we'd need either pre-determined names or a different at-rule.

@context(first) {
  /* resets/defaults go here */
}

/* all regular styles will fit in between */

@context(last) {
  /* user styles go here */
}

There's a lot of bikeshedding opportunities, but hopefully the idea is clear: @context would be a thing that lives one level above @layer. This also means @layer can still be used to organize styles within each context.

mirisuzanne commented 4 months ago

I find the @context specifics a bit hard to track - maybe because I don't usually think of page/shadow contexts being ordered. But I suppose it's reasonable to say the order is something like:

  1. normal shadow styles (lowest cascade priority)
  2. normal page styles
  3. important page styles
  4. important shadow styles (highest cascade priority)

So your proposal would result in…

  1. normal first-context styles (from either shadow or page?)
  2. normal shadow styles
  3. normal page styles
  4. normal last-context styles (from either shadow or page?)
  5. important last-context styles (from either shadow or page?)
  6. important page styles
  7. important shadow styles
  8. important first-context styles (from either shadow or page?)

But if styles can be added to a first/last context from either the light or shadow DOM… does their originating context still come into play? Or do we now ignore the shadow/page context, and conflicts within first/last have to continue in the cascade?

mayank99 commented 4 months ago

@mirisuzanne Thanks for looking into this. If you think this idea has legs, should I open a new issue specifically for @context?

But if styles can be added to a first/last context from either the light or shadow DOM… does their originating context still come into play? Or do we now ignore the shadow/page context, and conflicts within first/last have to continue in the cascade?

I want to say the originating context should not matter because it's a totally different context. The alternative would be that @context styles added from the innermost shadow context would cascade first.

In https://github.com/w3c/csswg-drafts/issues/6323#issuecomment-2181121893, I was also wondering if maybe @context should not be allowed to be used from shadow context. This sidesteps some of harder questions and can be reasoned about since the shadow styles are already in a fully encapsulated special context. Although, I think it would be confusing if some CSS features literally did not work in shadow DOM.

As a solution to static top/bottom layers… I would rather see that built into the @layer rule.

We could also have something built into @layer. The @context proposal does not have to replace my @layer initial proposal or any of the !override-like syntaxes proposed in #6323.

I do see the additional use of specifying something akin to presentational-hints, but in my mind this confuses the current meaning of a context, since styles would now move across contexts no matter where defined?

I only picked "context" because it seems like the right layer (no pun intended) to place "first"/"last" styles. What would be other ways of solving the problem I described in my earlier comment? Is it possible to make @layer initial cascade before the shadow context? Maybe @layer initial should actually refer to the presentational-hints layer?

mayank99 commented 4 months ago

Based on https://github.com/w3c/csswg-drafts/issues/6323#issuecomment-2207341923, I guess the original proposal in this thread could also be renamed to something like @layer !defaults. The only downside is it's not possible to easily polyfill.

I still think the @context idea solves something that the original proposal doesn't currently solve. Ideally, @layer !defaults would be placed alongside or prior to the presentational hints layer, so that it cascades before the shadow context.

It makes sense to place it before presentational hints, because I expect default styles to have the lowest priority in the author origin, and be overridden by preshints. As an added benefit of this, the behavior of revert-layer on the first non-default layer would remain the same (i.e. it doesn't revert preshints). I'm just not sure if it's possible from an implementation perspective.