w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.44k stars 657 forks source link

[css-selectors] Proposal: Selector Boundary #5057

Open jackfrankland opened 4 years ago

jackfrankland commented 4 years ago

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 https://github.com/w3c/webcomponents/issues/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 commented 4 years ago

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 commented 4 years ago

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:

Loirooriol commented 4 years ago

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 commented 2 years ago
  • 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.