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

When using @import scope(), are the imported styles 'nested'? #11756

Open
mirisuzanne opened this issue Feb 20, 2025 · 8 comments
Open

When using @import scope(), are the imported styles 'nested'? #11756

mirisuzanne opened this issue Feb 20, 2025 · 8 comments

Comments

@mirisuzanne
Copy link
Contributor

In #11237 @andruud's draft language for @import scope() includes the following note:

Note: While the [=style rules=] within the imported stylesheet
become [=scoped=],
they do not become [=nested style rule|nested=].
In particular,
top-level selectors are not re-interpreted as [=relative selectors=],
and the ''&'' pseudo-class maintains its non-nested behavior.

This behavior wasn't discussed is the original issue #7348

There has been some discussion on the PR, but this seems like a better place to have the full conversation and come to a resolution. Applying scope without nesting would be a departure from the behavior of @scope rules.

@romainmenke points out that the difference makes it hard for tooling to merge files, and wrap the imported sheet in a scope block. And Anders says:

We'd have to allow relative selectors top-level, but we could do that and spec that they are relative against :scope.

(Re-interpreting & is deeply incompatible with Blink's implementation of nesting, but we don't have to let that guide the discussion.)

@andruud
Copy link
Member

andruud commented Feb 20, 2025

We'd have to allow relative selectors top-level

Well, I guess we don't have to, but then it wouldn't be "nested" with all that it currently implies, it would be something new.

If the imported stylesheet is truly "nested", then even declarations would be allowed top level [1].

@romainmenke
Copy link
Member

@sheet could offer a way forwards to have both.

index.css

@import "foo.css" scope(.bar)

foo.css

a { color: green; }

Could be merged as:

@sheet some-sheet-id {
  a { color: green; }
}

@import some-sheet-id scope(.bar)

This implies that tools can't bundle @import scope() for a while, until support for @sheet is sufficient, but this is a (relatively) short term pain.


I can also see use cases for both behaviors:

1: A CSS author might be importing a 3rd party stylesheet and might want to scope it.
The author of the 3rd party sheet would not have written it with scoping in mind and applying it as it were wrapped in @scope might give unexpected results.

2: A CSS author might organize their files so that each component has their own stylesheet. Importing each with a @import scope(.my-component) condition seems like a very natural thing to do. Getting different behavior from wrapping in @scope (.my-component) would be surprising for this author.

@mirisuzanne mirisuzanne moved this to In progress in Cascade 6 (Scope) Feb 20, 2025
@LeaVerou
Copy link
Member

It would be helpful to list out the alternatives explicitly, as I for one, am not 100% sure I understand it correctly.

My understanding is that if the imported styles are not nested, it would require an explicit :scope or & to have them actually scoped to the selector specified in scope(). If that's correct, I think it is very surprising, and defeats the entire purpose of @import url(...) scope(...);. Literally all use cases I have come across (and there are many) the use case was that I wanted to import a stylesheet not written for @scope and have it interpreted as scoped to the specified selector.

That said, there is utility in being able to "escape" the scope and specify root values as well. What if :root can still match the document root? Has the ship sailed for that?

@mirisuzanne
Copy link
Contributor Author

mirisuzanne commented Feb 26, 2025

@LeaVerou your questions cover a lot of different ground, but let me see if I can clarify.

Selectors inside an @scope rule currently:

  1. Only match when the selector subject is inside the scope. (This is what it means to be a scoped selector)
  2. Imply a descendant combinator at the start of the selector, unless & or :scope is explicitly placed. (this is how 'nested' selectors work)

The scope at-rule applies both scoping and nesting to selectors inside. There is not a way to 'escape the scope' in the first sense. If the subject shouldn't be inside the scope, then the selector doesn't belong in a scope rule. But in the second sense it works like any other nested selectors. By default, we assume an implied descendant combinator - but it can also be placed explicitly:

@scope (article) {
  h2 { /* implicit `:scope h2` */ }

  main :scope h2 { /* explicit, as written */ }
}

In that sense, you can have complex selectors that escape the scope to establish context. And yes, :root still matches the document root.

One way to implement scoped imports would be to apply only the scoping, but not the nesting. For sure we want to require all matched subjects are in-scope. So the question is: do we also imply an implicit descendant combinator to all selectors in the imported document, unless they explicitly contain either :scope or &?

/* importer.css */
@import 'to-be-imported.css' scope(aside);

/* to-be-imported.css */
section h2 { … }

What does above section h2 match?

  • If we only apply scoping, we match the selector normally, but only when the matched h2 is also in scope. So both :scope section h2 and section :scope h2 results are valid.
  • If we also apply nesting, we imply :scope section h2 and would not match section :scope h2. To achieve that, the :scope would have to be placed in the selector explicitly.

If we want this to be consistent with the at-rule, we should apply both scoping and nesting. But the nesting changes the meaning of selectors, so it feels a bit more invasive. You would want to write the imported stylesheet with nesting in mind. It's not simply limiting the results to subjects that are in our desired scope.

As a side note: if we do decide to nest imported style sheets, maybe we also would want to allow @import 'my.css' nested(article)? That's often been requested with a different syntax, nesting the import rule inside a selector.

@romainmenke
Copy link
Member

@mirisuzanne thank you for this explanation, this also makes it more clear where exactly the difference is noticeable.

Is it then correct that any selector containing either & or :scope will behave the same even when import with scope() doesn't apply nesting?

I think it could be an acceptable tradeoff for bundlers to require authors to always add an explicit & or :scope. That doesn't actually limit any capability, it only requires authors to do a bit more typing, right?

@mirisuzanne
Copy link
Contributor Author

@romainmenke I don't think I understand what you are trying to do. I don't see what that achieves, and I'm not sure the logic is quite right. But I also think we need to agree on the most useful CSS behavior, before we spend too much time worrying about how to polyfill it?

Currently, selectors at the top level of a CSS document are not 'relative' to anything. They can't, for example, start with a combinator. Nested selectors are relative to the parent selector. Relative selectors often start with a combinator:

> h2 { /* not a valid selector */ }

main {
  > h2 { /* nesting makes it valid, with implied `&`  */ }
}

The first selector here is currently invalid if we apply the stylesheet directly. Would it suddenly become valid when the stylesheet is imported with nesting applied?

@LeaVerou
Copy link
Member

LeaVerou commented Feb 27, 2025

I see, thanks for the explanation. I still maintain that to the extent possible, rules should behave identically to being nested in an @scope block. That seems to be a clearer mental model, and it doesn't seem there is a strong justification for the alternate behavior to warrant a departure from it.

Even consistency aside, that seems like the clearer mental model. If authors desire the other behavior where the rest of the selector is essentially a filter, they can make their intent clear by rewriting section h2 as h2:is(section *).

I think it could be an acceptable tradeoff for bundlers to require authors to always add an explicit & or :scope. That doesn't actually limit any capability, it only requires authors to do a bit more typing, right?

Like @mirisuzanne, I also don't understand what purpose this serves. Additionally, I see being able to repurpose existing stylesheets as an important use case for this feature, so anything that requires writing them differently eliminates this.

Currently, selectors at the top level of a CSS document are not 'relative' to anything. They can't, for example, start with a combinator.

This seems orthogonal and likely warrants a new issue: why aren't they valid? We resolved a while ago that outside of nesting, :scope and & resolve to :root. If > h2 is essentially equivalent to & > h2, why wouldn't that be valid in the root scope?

Another orthogonal issue is that many stylesheets define CSS variables under :root, which would not match anything for either condition. Perhaps the answer to this is just "they should transition to using :scope instead, but I wonder if it may make sense to make :root resolve to :scope when used inside an @scope rule.

@mirisuzanne
Copy link
Contributor Author

This both seem like good issues to open, and potentially consider as part of a scope break-out session.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: In progress
Development

No branches or pull requests

4 participants