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-selectors] Proposal: Selector Boundary #5057

Open
jackfrankland opened this issue May 9, 2020 · 4 comments
Open

[css-selectors] Proposal: Selector Boundary #5057

jackfrankland opened this issue May 9, 2020 · 4 comments

Comments

@jackfrankland
Copy link

An element can be marked as having a selector boundary:

<div boundary="my-element">
  <h1>My Element</h1>
</div>

This states that a css selector will not match the element unless the correct boundary is specified (please note use of pseudo class is very much tentative):

  // will not match:
  div h1 {
    color: red;
  }

  //will match:
  :boundary(my-element) h1 {
    color: green;
  }

If the tag name contains a hyphen, and a boundary attribute is present with no defined value:

<my-element boundary>
  <h1>My Element</h1>
</my-element>

Use of the tag name in a selector will be a valid match:

// won't match
h1 {
  color: red;
}

// will match
my-element h1 {
  color: green;
}

Why?

Using Shadow DOM to encapsulate styles may not be the optimal solution for all use cases. For a component-based application where the author has full control, the concern is often limited to preventing styles defined for one component leaking into others. This has led to CSS-in-JS solutions to emulate scoping. While these solutions have other merits (modularisation and tree-shaking), downsides can be, depending on the solution and viewpoint: a tighter coupling between css and a component's template, or an inability to define styles for an element and its descendants in the standard, declaritive, way. This proposal offers a way for selectors defined in global stylesheets to not apply beyond defined boundaries, effectively enabling style scoping for components.

Another reason is to allow for theming of embedded library components. As discussed in WICG/webcomponents#864, there is a desire to allow greater control of styling for users of component libraries. I share the opinion with others in that discussion that allowing the shadow root to be pierced will be detrimental to the maintainability of the component. Still, a component author may wish to distribute a component with the assurance that outside styles will not leak into it by default, while knowingly allowing the styles for the component to be overridden freely by the user of the library. This doesn't necessarily have to replace the need for greater theming of a shadow root, but it does give the component author more flexibility if they feel using Shadow DOM isn't an optimal solution for the situation.

Thirdly, and not much thought has gone into this so it may be a bit of a stretch... it could enable better SSR of elements that will eventually attach shadow roots on script execution. With selector boundaries superficially encapsulating styles only, it may be possible for the initial render to utilise them, before hydrating the elements with their shadow roots.

Other details

Nested boundaries

When a boundary is specified for an element that is a descendant of an element that has its own boundary:

<my-grandparent boundary>

  <my-parent boundary>

    <my-child boundary>
      <h1>My Child</h1>
    </my-child>

  </my-parent>

</my-grandparent>

A selector only has to match the single most-relevant boundary to be valid, not the full hierarchy:

// will match
my-child h1 {
  color: green;
}

// will not match
my-parent h1 {
  color: red;
}

// will match but is not necessary
my-grandparent my-parent my-child h1 {
  color: green;
}

Inheritance

Allowing for inheritance to pass through the boundary would depend on if there are any use cases where it would be wanted, and may be a separate concern. Preventing it can already be handled quite easily with my-element { all: initial; }

Default boundary for custom elements

A custom element could be marked with a boundary quite easily:

class MyElement extends HTMLElement {
  connectedCallback() {
    this.setAttribute('boundary', ''); // perhaps a new "boundary/selectorBoundary" property instead
  }
}

Alternatively, the following could be supported:

customElements.define('my-element', MyElement, { selectorBoundary: true });
@Loirooriol
Copy link
Contributor

the concern is often limited to preventing styles defined for one component leaking into others

Scoped styles seem a better solution to me, #3547

@jackfrankland
Copy link
Author

Scoped styles seem a better solution to me

Thanks for your feedback. Just want to check, is it that you prefer what scoped styles allows you to do, rather than the method? As far as I understand, there's a slight difference between that and what this proposal is trying to achieve:

  • Scoped styles will not prevent outside styles from leaking into the scope, while not allowing outside styles to override defined styles within the scope
  • The proposed "boundary" here will prevent outside defined styles from leaking into the boundary by default, while allowing styles to be set from the outside with an explicit selector

@Loirooriol
Copy link
Contributor

That's right, they are not the same. Instead of adding a boundary to avoid undesired styles, you would scope these undesired styles to another subtree (which may not always be doable).

IMO your proposal makes selectors more confusing. Currently the selector model is not that complex, the simple selectors in a compound selector just impose additional constraints to the same element, and combinators specify the relationship between the elements matching the adjacent compound selectors.

With your proposal, the set of elements matched by a simple selector depends on whether the full selector contains some specific simple selector. From another point of view, type/pseudo-class selectors are no longer filtering the elements matched by *, they can also affect other simple selectors in the same selector.

There are already common misconceptions about the current model, I worry it may become worse with your proposal. Scoped styles seem less problematic to me, and can cover some of your usecases.

@DarkWiiPlayer
Copy link

DarkWiiPlayer commented Jan 25, 2022

  • Scoped styles will not prevent outside styles from leaking into the scope, while not allowing outside styles to override defined styles within the scope

Correct me if I'm wrong, but couldn't you achieve almost the same effect with something like this?

@scope ([boundary="my-element"]) {
  * { all: initial }
  /* scope-specific styles go here */
}

The idea of "outside styles" doesn't really mean much in practice, as the scoped rules wouldn't have to be in a separate file or otherwise separated from the "outside" CSS, and the additional block and level of indentation actually make it more readable in my opinion.

@fantasai fantasai added selectors-5 and removed selectors-4 Current Work labels Nov 8, 2022
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