Skip to content

[css-cascade-6] Scoped selectors shouldn't match the scope root unless explicitly requested with :scope? #8377

Closed

Description

@mirisuzanne

This was suggested by @bramus in an offline conversation.

In the current cascade-6 specification, the @scope rule defines a 'scope' as a tree fragment that includes the scope root (:scope) element, but does not include any lower-boundary elements, or their decedents. Since the scope-root is 'in scope', it can be matched by scoped selectors:

@scope (div.scope) {
  /* This selector will match the :scope element */
  div { ... }

  /* :scope can be explicitly excluded */
  div:not(:scope) { ... }
}

While it's possible to exclude the scope-root from selectors by appending :not(:scope) - we could consider doing it the other way around. For example, a 'shadow host' is not matched by selectors in the shadow DOM unless :host is explicitly specified. The scope-root would still need to be 'available' to scoped selectors, but it would only match when specifically mentioned:

@scope (div.scope) {
  /* This selector WOULD NOT match the :scope element */
  div { ... }

  /* :scope can be explicitly included */
  div, :scope { ... }
}

On the one hand, it may be strange to have an element that is available 'in scope' but not matched by default. On the other hand, authors are likely to expect that matched elements will be 'nested' inside the scope-root selector. In either case, we would have a reasonable workaround for the counter-examples.

Activity

  1. tabatkins commented on Jan 30, 2023

    @tabatkins
    Member

    The other way to exclude the scope is just * > ..., right? And when nesting you can just do > ..., which might be simple enough to be worth leaving it as the way to handle things.

    Note that the host element is unmatchable except by the :host pseudo; it's not contextual in the selector itself. That is, you can't write :host.foo - by definition the .foo won't match the host element. I suspect that it'll be relatively common to want to match additional selectors against the scope, tho. For host elements we allow that with a functional form - :host(.foo), but we don't (yet?) have a :scope() functional form.

    I lean weakly towards "no change", then.

  2. mirisuzanne commented on Jan 30, 2023

    @mirisuzanne
    ContributorAuthor

    The other way to exclude the scope is just * > ..., right? And when nesting you can just do > ..., which might be simple enough to be worth leaving it as the way to handle things.

    Not quite - though your point may still stand. Only the matched element needs to be in scope, the rest of the scoped selector matches unrestricted – so you would need to explicitly list the scope root itself as the ancestor: :scope * would match everything in-scope except for the scope root.

  3. mirisuzanne commented on Jan 30, 2023

    @mirisuzanne
    ContributorAuthor

    I suppose > * might imply a starting &, which would be treated as :scope in this case? But * > * could still match the scope root itself.

  4. tabatkins commented on Jan 30, 2023

    @tabatkins
    Member

    Oh duh, right. Well yeah, at least the Nesting version would work by default, yeah.

  5. romainmenke commented on Jan 30, 2023

    @romainmenke
    Member

    I suppose > * might imply a starting &, which would be treated as :scope in this case?

    How?
    Do you have a full example?

    /* relative selector is not allowed here */
    > * {}
    
    @scope (div.scope) {
      /* relative selector is not allowed here */
      > * {}
    }
    
    .bar
      @scope (div.scope) {
        /* relative selector IS allowed here, but & is `.bar` */
        > * {}
      }
    }

    In the last example it would be .bar > * where anything that matches must also be within scope div.scope right?

  6. bramus commented on Jan 30, 2023

    @bramus
    Contributor

    Thanks @mirisuzanne for posting this issue. It was still on my todo, but glad to see you posted it (in a more articulate way than I ever could).

    The idea came up after playing with the @scope prototype in Chrome and somewhat at the same time thinking about sibling scopes. For sibling scopes it would seem very weird that one of the boundaries is included by default and the other not. To make sense, both boundaries should either be included, or both excluded.

    With regular @scope excluding the end boundary by default, excluding the start boundary seemed like the logical thing to do in order to reach a balanced type of selection. From thereon out :scope came to mind to target the start boundary element itself.

    One thing that might seem a bit weird in this scenario is that there’s only one pseudo available to select the start boundary, not one to select the lower boundary. Miriam had arguments that this wasn’t too big of a problem.

  7. mirisuzanne commented on Jan 30, 2023

    @mirisuzanne
    ContributorAuthor

    @romainmenke my understanding was that we had decided to make & valid everywhere. According to the Editor's Draft:

    When used in the selector of a nested style rule, the nesting selector represents the elements matched by the parent rule. When used in any other context, it represents the same elements as :scope in that context (unless otherwise defined).

    I suppose it's not clear to me if you're allowed to leave the & off those selectors, when not in a nested style rule? Maybe that needs to be clarified? At the very least, all your example selectors seem to be allowed if you start them with &, which is normally implied by a relative selector.

    One thing that might seem a bit weird in this scenario is that there’s only one pseudo available to select the start boundary, not one to select the lower boundary. Miriam had arguments that this wasn’t too big of a problem.

    @bramus Yeah, that might be a little odd, and may point to a :boundary selector being useful (name TBD of course).

  8. romainmenke commented on Jan 30, 2023

    @romainmenke
    Member

    I suppose it's not clear to me if you're allowed to leave the & off those selectors, when not in a nested style rule? Maybe that needs to be clarified? At the very least, all your example selectors seem to be allowed if you start them with &, which is normally implied by a relative selector.

    I suggested to allow relative selectors in more contexts here : #8010
    But this is a bit of a tangent :) So maybe worth revisiting that issue in case it could be helpful for @scope.

  9. mirisuzanne commented on Feb 14, 2023

    @mirisuzanne
    ContributorAuthor

    Digging into this, I'm convinced that authors will expect 'nested by default' behavior - which would better match how nesting works as well. To get there, we don't actually want to remove the scope root from the scope. We only want to imply that the scoped element is prepended as an ancestor, unless otherwise explicitly placed in a selector. This would bring :scope and & more fully in line. Both already have the same specificity, and the same is-like parent-element matching logic already.

    This is a bit different from the 'virtual' scoping root behavior defined for dom fragments, the primary use of :scope currently. According to the spec, document fragment roots can be mentioned in a scoped selector, but can't be the subject of the selector. If we have to match that behavior, I think it would be too restrictive.

    If we go that rout, it's not an exact parallel with :boundary or :scope-end. On the root side, we're defining an implied ancestor/descendant relationship between selectors. On the lower boundary, we would have an element that is fully out of scope for some selectors, and fully in scope for other selectors. That might also be useful, but it would need a different mechanism for describing the behavior.

  10. tabatkins commented on Feb 14, 2023

    @tabatkins
    Member

    According to the spec, document fragment roots can be mentioned in a scoped selector, but can't be the subject of the selector. If we have to match that behavior, I think it would be too restrictive.

    Yeah, that restriction is only because doc fragments aren't elements, and selectors are defined to operate over elements. It shouldn't be taken as a more general restriction on :scope.

  11. mirisuzanne commented on Feb 15, 2023

    @mirisuzanne
    ContributorAuthor

    In that case, my proposed resolution is that scoped selectors are implicitly descendants of the scope root element, unless :scope appears explicitly in the selector - matching the behavior of nested selectors with or without the explicit &.

    I think something like :scope-end might be useful, but could have it's own issue.

  12. mirisuzanne commented on Feb 16, 2023

    @mirisuzanne
    ContributorAuthor

    Agenda+ to resolve on that change.

  13. andruud commented on Feb 16, 2023

    @andruud
    Member

    +1, this is great. I think it opens up some new ways of optimizing.

    unless :scope appears explicitly in the selector

    This means we'll add something like the "nest-containing" concept but for scoped selectors, right?

  14. 4 remaining items

  15. tabatkins commented on Feb 16, 2023

    @tabatkins
    Member

    Agreed, I think 2a is the clearest - maintains & semantics from general usage, but also interfaces with @scope in a reasonable and intuitive way.

  16. andruud commented on Feb 16, 2023

    @andruud
    Member

    [Why is it that the second css-nesting comes into the picture there's a list with options and suboptions? :P]

    (2a) I'm not sure it's very consistent to disable the implied :scope when nothing in the selector actually represents :scope. 2b seems more consistent.

  17. mirisuzanne commented on Feb 16, 2023

    @mirisuzanne
    ContributorAuthor

    Yeah, 2b is semantically 'cleaner' in some ways (both selectors maintain their behavior), but I think this would be very surprising to authors, and not a meaningful way of supporting & in a scoped context. Authors will expect these to reference the same elements:

    .media {
      & > img { ... }
    }
    
    @scope (.media) {
      & > img { ... }
    }

    In the current spec, that works because we don't imply an initial :scope. But if we go with 2a, we've broken that expectation.

  18. bramus commented on Feb 17, 2023

    @bramus
    Contributor

    Authors will expect these to reference the same elements:

    .media {
      & > img { ... }
    }
    
    @scope (.media) {
      & > img { ... }
    }

    That’s also the case with option 1, no? The way I read that option, is that inside @scope (…) { … } the contents are implicitly wrapped in a :scope { } block?

    So that this … :

    @scope (.media) {
      & > img { ... }
    }

    … equals that:

    @scope (.media) {
      :scope {
        & > img { ... }
      }
    }

    That would essentially make :scope useless inside @scope, as one would need to use & instead … but maybe that’s OK?


    [Why is it that the second css-nesting comes into the picture there's a list with options and suboptions? :P]

    🙈

  19. bramus commented on Feb 17, 2023

    @bramus
    Contributor

    That would essentially make :scope useless inside @scope, as one would need to use & instead … but maybe that’s OK?

    Come to think of it: for @sibling-scope that might be weird, as one might want to target the start and end boundaries there. Using & for the start boundary and something like :scope-end for the end boundary seems inconsistent.

  20. mirisuzanne commented on Feb 17, 2023

    @mirisuzanne
    ContributorAuthor

    I think in both css nesting and scope we want to think of it as an implicit descendant combinator, not an implicit extra level of nesting.

  21. mirisuzanne commented on Feb 22, 2023

    @mirisuzanne
    ContributorAuthor

    The proposed resolution here is:

    • Scoped selectors are implicitly descendants of the scope root (this matches css-nesting)…
    • Unless they are scope-containing, with either :scope or & explicitly placed in the selector (the matches and expands on css-nesting)
    • In a scoped selector, :scope refers to the scope root element (cannot match additional nested elements), while & refers to the scoping selector (can match additional nested elements).

    This builds on (and clarifies) the resolution in #7854 that & should be allowed in a scoping context. It also ensures that scope and nesting behave in similar ways where they overlap.

  22. bramus commented on Feb 28, 2023

    @bramus
    Contributor

    Proposed resolution SGTM.

    Maybe we can also consider :scope-end to target the end boundary along with this as it’s very closely related? Happy to branch off a new issue (as suggested here) if we can’t tackle it all in one go.

  23. css-meeting-bot commented on Mar 1, 2023

    @css-meeting-bot
    Member

    The CSS Working Group just discussed [css-cascade-6] Scoped selectors shouldn't match the scope root unless explicitly requested with :scope?, and agreed to the following:

    • RESOLVED: clarifications for scoping selectors proposed by miriam
    The full IRC log of that discussion <fremy> miriam: the scope roots elements are currently part of the scope, with no implied relationship
    <fremy> miriam: so, if you use the same selector for both ends, the end matches the root
    <fremy> miriam: bramus and others found that confusing
    <fremy> miriam: so the proposal would be to imply that nesting scope selectors include :scope
    <fremy> miriam: except if they explicitly refererence :scope or use ampersand
    <fremy> miriam: there are a couple of differences between :scope and ampersand
    <TabAtkins> & references all the elements matched by the scope-start selector (as normal for Nesting), :scope matches the specific element that is functioning as the scope in a given element's context.
    <fantasai> +1 to the proposal
    <fremy> miriam: mainly you can use & multiple times
    <TabAtkins> +1 to the proposal
    <fremy> miriam: so the proposal clarifies the difference
    <astearns> ack fantasai
    <fremy> fantasai: what is the specificity of :scope and ampersand?
    <fremy> miriam: there is another issue on that
    <fremy> miriam: but my guess is that ampersand should work just as in nesting
    <fremy> miriam: (wrapping the starting scope selector in :is)
    <bramus> q+
    <fremy> TabAtkins: (missed short comment)
    <fremy> astearns: does that work for you fantasai ?
    <astearns> ack bramus
    <fremy> fantasai: yes
    <TabAtkins> TabAtkins: That's also my preference
    <fremy> bramus: should we also introduce :end for the end boundary?
    <fremy> bramus: for sibling scopes, that might be useful
    <fantasai> proposal was to have & use the specificity of the selector it's referencing, and for :scope to have its normal specificity (1 pseudo-class)
    <fremy> miriam: this sounds like a separate resolution
    <fremy> miriam: could you provide some clear use cases for that?
    <TabAtkins> have we discussed the meaning of relative selectors like `> .foo`? Do those get captured by Nesting, implying an &, or do they imply a :scope? I think the latter should be the case.
    <fremy> astearns: the proposed resolution is in the last comment of the thread
    <fremy> astearns: looking at tab's question on irc
    <fremy> astearns: tab, do you want to voice the question?
    <fremy> miriam: I think I agree with TabAtkins suggestion
    <fremy> miriam: relative selectors (...missed)
    <fremy> miriam: question was what happens if the selector starts with a descendant combinator
    <fremy> miriam: so just adding a descendant combinator should be the same as the default
    <astearns> ack fantasai
    <fremy> fantasai: I think that it implies that every selector inside a scope that doesn't start with an ampersand has an additional pseudo-class specificity
    <fremy> TabAtkins: no, no difference
    <fremy> TabAtkins: the implied-ness is just for the meaning, but not the specificity
    <fremy> TabAtkins: the implied one does not add specificity
    <fremy> fantasai: p and :scope p are the same, but with different specificities
    <fremy> TabAtkins: yes
    <fremy> fantasai: and if I add (...) I could match more
    <fremy> TabAtkins: yes, not in that use case, but in others yes
    <fremy> fantasai: if you use descendants, you can match much more
    <fremy> fantasai: and both :scope and & can match the scoping element, but no other selector can
    <fremy> miriam: no other selector make it remove the nested-within combinator
    <fremy> TabAtkins: by the magic of relative selectors, it will never happen
    <fremy> TabAtkins: you have to make it absolute to override
    <fremy> fantasai: ok, this is probably fine, but the spec should be extra clear about it
    <fremy> astearns: so, the proposed resolution is to accept miriam's prososal + add new clarifications
    <fremy> astearns: any comment?
    <fremy> fantasai: lgtm
    <fremy> astearns: any objection?
    <fantasai> s/lgtm/I think this is a good change and I support it/
    <fremy> RESOLVED: clarifications for scoping selectors proposed by miriam
  24. added a commit that references this issue on Mar 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions