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] Can we use @scope for style isolation? #11002

Open
bramus opened this issue Oct 4, 2024 · 16 comments
Open

[css-cascade] Can we use @scope for style isolation? #11002

bramus opened this issue Oct 4, 2024 · 16 comments

Comments

@bramus
Copy link
Contributor

bramus commented Oct 4, 2024

CSS @scope is for selector isolation, not style isolation. Properties that inherit will inherit onto children beyond the scope’s scoping limit. From a lot of authors I hear that they expected it to be about style isolation.

I was wondering if it would be possible to extend @scope to do style isolation too – something I believe would be very helpful for web component authors.

Maybe a prelude like isolated? E.g. @​scope isolated (root) to (limit) { /* no authors styles get in or bleed out */ }.
Or maybe there need to be two flags? One to prevent styles from getting in, and one to prevent them from bleeding out?

@romainmenke
Copy link
Member

What does "bleed" mean exactly in the context of this issue?
I assume that inheritance would still work the same?

@bramus
Copy link
Contributor Author

bramus commented Oct 4, 2024

With it I mean to stop inheritance at the set scoping limit (if any).

@romainmenke
Copy link
Member

Maybe a revert-scope keyword would be better?
This seems very similar to all: revert and all: revert-layer.

@sorvell
Copy link

sorvell commented Oct 7, 2024

Is the intention that this would only change the behavior of property inheritance or would it also interact with selector matching?

If it is only for inheritance, how is it different from @scope (...) { :scope { all: reset; } }?

Given:

<style>

.root {
  font-family: sans-serif;
  --foo: 1px;
}

.foo {
  color: blue;
  --bar: 2px;
  background: red;
}

@​scope isolated (.root) {}

</style>
<div class="root">
  <div class="foo">how do I look?</div>
</div>

Some questions about div.foo:

  1. would it have font-family: sans-serif;? ... presumably no?
  2. would it have color: blue;?
  3. would it have background: red;?
  4. same questions for --foo and --bar

@zaygraveyard
Copy link

I might be wrong but I believe @bramus meant something like the following.

Given:

<style>

:root {
  color: green;
}
@​scope isolated (.root) to (.foo) {
  :scope {
    color: red;
  }
}

</style>
<div class="root">
  this text is red
  <div class="foo">this text should be green</div>
</div>

div.foo's color will be inherited from :root instead of .root because the @scope was isolated.

@mirisuzanne
Copy link
Contributor

We'll want to be careful about naming something like this. Isolation might imply to authors that styles from 'outside' won't inherit into the scope. That sort of isolation-from-outside by definition has to be handled from the DOM. We can't have some style rules blocking other style rules from matching the same elements. This feature is limited to scoping the declarations that are defined inside the scope rule, so they don't get out.

But even then, I'm not sure how we would actually want this to work. When a non-scoped element 'slotted' into our scope is defaulting, and would normally inherit… now what?

I would expect that I maybe inherit from the parent of the scope root or something like that? We ignore the styles on any elements in our scope, and inherit styles from elsewhere on the page. I think this is somewhat intuitive, and maybe what you're asking for? But I don't think it works with a selector-based scope. We inherit values from elements, not from declarations. What do we do with values that were applied from out-of-scope, but apply to in-scope elements? What if those values were discarded in the cascade?

To do that consistently, we wouldn't be ignoring only the scoped declarations, we would also have to ignore un-scoped (or un-isolated) declarations that match in-scope elements.

<style>
:root { color: green; }
.scope-root { color: teal; }
.in-scope { color: blue; }

@​scope isolated (.scope-root) to (.foo) {
  .in-scope { color: hotPink; }
}

</style>
<div class="scope-root">
  This text is teal
  <div class="in-scope">
    <div class="foo">What color is this??</div>
  <div>
</div>
  • It can't be hotPink, that was isolated
  • Probably not blue? We can't inherit a color that was never applied, right?
  • We could look at the parent to get teal, but is that confusing? Why would we inherit from an element that we think of as in-scope?
  • We could skip over that, and use green, but that's even more confusing. Does our scope rule isolate styles that aren't even scoped??

I think the only way to make sense of this is that elements/properties need to opt out of inheritance explicitly - and specify a new place to get the value from.

A new global revert-scope keyword might be part of that. When that is the cascaded value, we would have to ignore any values set in the 'same' scope rule – and default to values from elsewhere. 'Same' here would have to be lexically defined as the nearest (nesting-wise) at-scope rule block where the keyword is being applied. That means the result is not an implicit 'inheritance' from somewhere else, but an explicit defaulting, like we get from revert-layer.

I like that this syntax clarifies where the new default will come from. But this approach comes with a big limitation. The lower boundary elements are excluded from the scope, so they can't be styled from the 'same' scope as the styles we want to revert. It would not be possible to have all the lower-boundary selectors revert-scope – unless we also provide a :scope-end selector.

Maybe we should provide that anyway? See #8617


But I wonder if we really want (or also want) a way to 'inherit from named ancestor x'?

@mirisuzanne
Copy link
Contributor

mirisuzanne commented Oct 9, 2024

Even with revert-scope and :scope-end, I'm not sure we would get something that matches what you're asking for though:

<style>
:root { color: green; }
.scope-root { color: teal; }
.in-scope { color: blue; }

@​scope isolated (.scope-root) to (.foo) {
  .in-scope { color: hotPink; }
  :scope-end { color: revert-scope; }
}

</style>
<div class="scope-root">
  This text is teal
  <div class="in-scope">
    <div class="foo">What color is this??</div>
  <div>
</div>

Since the color was never defined on .foo, we revert to no specified value, and still need a default from somewhere. Unless revert-scope (unlike revert-layer) is defined to revert even the values that would inherit? We're back in strange territory if we try that.

@zaygraveyard
Copy link

@mirisuzanne

  • It can't be hotPink, that was isolated

Correct

  • Probably not blue? We can't inherit a color that was never applied, right?

I would say that intuitively it should be blue, although I'm sure if that's compatible with the way CSS works 😅

  • We could look at the parent to get teal, but is that confusing? Why would we inherit from an element that we think of as in-scope?

I feel like that would be confusing, not because it's inheriting from an in-scope element, but because it's skipping the non-scoped color declared on it's parent .in-scope (i.e., blue)

  • We could skip over that, and use green, but that's even more confusing. Does our scope rule isolate styles that aren't even scoped??

I agree, that's even more confusing

Again, I might be wrong, but I believe that the original idea is to remove the isolated style declarations from the cascade for all elements outside specified scope, if that makes sense.

@mirisuzanne
Copy link
Contributor

The reasoning makes sense to me, but I don't know if the behavior would make sense in practice. It's sort of fundamentally not how inheritance works, which is to get the computed value on the parent after it's already been through the cascade etc. And in this case we'd be asking for a value that was never computed or applied anywhere, but is still coming from context.

@bramus
Copy link
Contributor Author

bramus commented Oct 11, 2024

Thanks for the example @mirisuzanne. I hadn’t really thought of the situation where you’d have non-scoped styles targeting elements inside the created scope. It can be interpreted in many ways, leading to surprising results.

@bramus
Copy link
Contributor Author

bramus commented Oct 14, 2024

To do that consistently, we wouldn't be ignoring only the scoped declarations, we would also have to ignore un-scoped (or un-isolated) declarations that match in-scope elements.

<style>
:root { color: green; }
.scope-root { color: teal; }
.in-scope { color: blue; }

@​scope isolated (.scope-root) to (.foo) {
  .in-scope { color: hotPink; }
}

</style>
<div class="scope-root">
  This text is teal
  <div class="in-scope">
    <div class="foo">What color is this??</div>
  <div>
</div>
  • It can't be hotPink, that was isolated
  • Probably not blue? We can't inherit a color that was never applied, right?
  • We could look at the parent to get teal, but is that confusing? Why would we inherit from an element that we think of as in-scope?
  • We could skip over that, and use green, but that's even more confusing. Does our scope rule isolate styles that aren't even scoped??

I remember now what I was originally thinking when I proposed this. My line of thinking was that the subtree with @scope isolate … would get yanked out of the main tree along with its styles, so in this example here you’d end up with essentially two trees being styled individually from each other.

  • tree 1:

    <style>
    :root { color: green; }
    .scope-root { color: teal; } /* Doesn’t match anything */
    .in-scope { color: blue; } /* Doesn’t match anything */
    </style>
    <div class="foo">What color is this??</div>
  • tree 2:

    <style>
    @​scope isolated (.scope-root) to (.foo) {
      .in-scope { color: hotPink; }
      :scope-end { color: revert-scope; }
    }
    </style>
    <div class="scope-root">
      This text is teal
      <div class="in-scope"><div>
    </div>

So the text in .foo would be green and the text in .scope-root would be the color that it inherits from the UA stylesheet.

@mirisuzanne
Copy link
Contributor

It doesn't seem to me like it's reasonable for some CSS to define what other CSS is allowed to style. I feel like that comes with all sort of spooky-action-at-a-distance implications. I can basically turn off all your existing styles by saying @scope isolate (:root) { … }.

@sorvell
Copy link

sorvell commented Oct 15, 2024

It doesn't seem to me like it's reasonable for some CSS to define what other CSS is allowed to style.

This is exactly what I was hoping was being proposed here. I recognize it seems like an exotic concept, but the existence of Shadow DOM style isolation shows that it's valuable in principle. And yes, that is dictated by the shape of the DOM, but it's exactly this restriction that makes it unwieldy and problematic.

Zooming out, CSS decouples styling from the DOM. It uses information in the DOM but it's not really limited by it (I can select a class or a tag name or an id etc.).

New concepts like @scope selectively apply rules to portions of the DOM and @layer provides explicit control over the order of the cascade. The isolation concept seems like a potentially logical next step.

Decoupling isolation from an intrustive DOM feature (Shadow DOM) would be more expressive and flexible. If there's not some fundamental reason it's antithetical to the core design of CSS, it seems at least worth considering?

@mirisuzanne
Copy link
Contributor

I'm not arguing with the usefulness of isolation in relation to CSS. I get why it sounds useful.

But… you're just entirely not worried about how this proposal allows some CSS to turn off all other CSS? Does that include browser defaults? What about user preferences? They aren't scoped. When I scope over a custom element, do I exclude shadow styles? Are important styles excluded as well? This feels like such a wildly powerful trump card, it requires a whole new cascade-of-scopes. I just have so many questions about how this would be done in a manageable way that doesn't lead to chaos.

So I guess I'm interested in a fleshed-out proposal before I comment more? Maybe you all see a path forward that I'm just missing.

@sorvell
Copy link

sorvell commented Oct 15, 2024

Does that include browser defaults? What about user preferences? They aren't scoped. When I scope over a custom element, do I exclude shadow styles? Are important styles excluded as well?

This part doesn't seem too tricky to me. It's an author styles concept so it only applies to those (default and user would still apply). Shadow DOM is separately isolated so it's unchanged. Important doesn't penetrate since I think for properties this would act like all:initial + custom properties.

I just have so many questions about how this would be done in a manageable way that doesn't lead to chaos.

Yup, I share the concern.

The isolation concept is inherently defensive and potentially implies that other style rules could be unknown "threats." This is probably a bad signal to send, although all: initial seems sort of similarly defensive in nature.

The recent additions of @layer and @scope seem more pro-actively oriented: I do something additive to "take control" of the cascade and region of influence.

With this in mind, I think another direction we could go to achieve a similar level of expressiveness and flexibility would be to have more power about bringing a set of rules into a scope. So instead of isolate a region we selectively include rules into that region. Perhaps this would be via @layer (or maybe this goes more with mixins?) For example:

  1. ensure layer can be used easily on all stylesheets (<style>, < link >)
  2. allow a layer to be "applied" to a scope:
<link rel=stylesheet href=... layer=foo>
<style>
@scope (.foo) {
    @mixin-layer(foo);
}
</style>

If this seems like a more reasonable direction, it probably needs its own issue (and/or there are likely other relevant issues).

@mirisuzanne
Copy link
Contributor

all:initial reverts to initial values for a property (eg display:inline) rather than default values for an element (display:block for paragraphs, display:none for head, etc). More likely desired result is either all:revert (browser defaults) or all:revert-layer (so authors can provide our own defaults in a lower layer). The latter is the most similar to the suggested behavior above - but treating the previous scope as if it's the layer to revert.

I'm not sure how selective inclusion is different from isolation? Those seem the same to me, or else separate features. Treating an import (or a layer or a sheet) as a mixin is interesting, but unless the scope is isolated from un-scoped styles, I'm not sure it changes anything here.

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

5 participants