Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
mayank99 opened this issue Mar 16, 2024 · 6 comments
Open

Comments

@mayank99
Copy link

mayank99 commented Mar 16, 2024

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:

  • If this layer order is not set upfront, then any styles that come before @layer defaults can potentially declare layers that come first.
  • It requires everyone to be aware of the name defaults if they want to declare low-priority styles later.
  • It hinders the adoption of libraries that want to use cascade layers, because they need to instruct page authors to set up layer order correctly.
  • Shadow-roots need to individually come up with their own defaults layer.

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.

  • Page authors and component authors alike can easily add styles to the initial layer, without having to come up with a carefully-coordinated layer setup.
    • @layer initial will provide a canonical way to deprioritize certain styles. It "always works" and is not impacted by order of appearance or existing layer architecture.
  • CSS “resets” (such as “CSS remedy”) can be pre-wrapped in @layer initial, with a guarantee that they will cascade first, regardless of how/where they are imported.
  • Component library authors can use CSS layers more liberally, without having to worry about third-party resets potentially being in higher-priority layer.
    • Page authors may not even need to care whether a library’s CSS uses cascade layers internally.
    • Library's CSS will be able to use an anonymous layer, with a guarantee that it will still cascade after the initial layer.
  • Outer context styles that are adopted into an inner context can use @layer initial for their default styles, even though .adoptedStyleSheets is ordered after the inner context’s own .styleSheets.
    • This is particularly important for open-styleable shadow roots, where document stylesheets may be ordered after shadow-root’s styles. The document's initial layer will still cascade before the shadow-root's layered and unlayered styles.

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.
@mayank99 mayank99 changed the title [css-cascade] Proposal: @layer initial always being the first layer [css-cascade] Proposal: @layer initial should always be the first layer Mar 16, 2024
@tabatkins
Copy link
Member

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:

  • sublayers of this initial layer are absolutely required. Nothing about an initial layer negates the need for organizing styles within the layer.
  • 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.

@mayank99
Copy link
Author

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
Copy link
Author

mayank99 commented Jun 19, 2024

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
Copy link
Contributor

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?

  • As a solution to static top/bottom layers… I would rather see that built into the @layer rule.
  • 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?

@mayank99
Copy link
Author

mayank99 commented Jun 26, 2024

@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 #6323 (comment), 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
Copy link
Author

mayank99 commented Jul 3, 2024

Based on #6323 (comment), 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.

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

No branches or pull requests

3 participants