w3c / csswg-drafts

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

[css-scoping] Proposal: Allow <slot> with LightDOM Web Components #10420

Open raphaelokon opened 5 months ago

raphaelokon commented 5 months ago

CSS evolved extensively in the last four years and gained a lot of features such as :has(), @container, @layer, @scope, CSS Anchor Positioning and @property(), to name a few.

These new CSS features change the concept of how we can write composable and high-distributable HTML with Web Components without always resolving to the highest power by utilizing ShadowDOM.

In the rise of design systems, there is currently no way to combine the powers of both ModernCSS and <slot>. This proposal aims to solve this problem.

Shadow Boundaries vs ModernCSS

Using ShadowDOM leads to a lot of new CSS features being rendered useless; to provide an incomplete list of problems identified so far:

  1. No containment queries (@container) across shadow boundaries of nested ShadowDOM Web components (was resolved via #5984 in spec but needs to be implemented across vendors).
  2. CSS Anchor Positioning does not work cross shadow boundaries.
  3. No use of :has() to style a parent depending on his slotted children (there is a proposal for a :has-slotted selector)
  4. No import of global @layer(). Each shadow-root recreates the semantics of the layers.
  5. No 100% CSS encapsulation as some styles pierce the shadow-boundary when slotting content into a shadow root.
  6. Using complex selectors like ::part that are limited in styling ShadowDOM content from the outside.
  7. Using CSS Custom Properties as an API to control styles in ShadowDOM contents is limiting (compared to global CSS).
  8. Additional: Constructable Stylesheets cannot use @import()
  9. Additional: Adopted StyleSheets have a bug in chromium

Solutions in the wild

There are articles in the web community that express the same sentiment about slots being the only-missing ShadowDOM feature.

Proposed solution

Since <slot>s are native to the ShadowDOM and cannot be polyfilled in the regular DOM (and style encapsulation can be achieved natively via modern CSS) the proposed solution is to let users use <slot> with LightDOM Web Components.

A simplified example with slots inside a <template> without attaching a shadowRoot

class MyCard extends HTMLElement {
    constructor() {
      super();
      let template = document.getElementById("card-template");
      let templateContent = template.content;

      /**
       * Append the template content and somehow 
       * reconstruct the flat-tree to allow slotting 
       * and live content projection.
       * @TODO: This needs to be fleshed out.
       */
      this.appendChild(templateContent.cloneNode(true);
    }
  }
  customElements.define("my-card", MyCard);
<!-- The card template  -->
<template id="card-template">
  <div class="card">
    <div class="card-header">
      <slot name="card-header"></slot>
    </div>
    <div class="card-content">
      <slot></slot>
    </div>
  </div>
</template>

<!-- Using the web component -->
<my-card>
  <h2 slot="card-header">My Card Header</h2>
  This is my regular card content.
</my-card>
  1. No changes needed to the existing ShadowDOM specification (e.g. adding mechanisms to let global styles pierce the shadow boundary with open-stylable via #10176).
  2. Developers can use all the new features such as @layer and @scope to provide a finer-grained CSS encapsulation.
  3. Developers can keep their mental model writing CSS (as in trusting the cascade), and not use tightly coupled selectors :host(parent-component) ::slotted(child-component) {…} to establish connections between nested ShadowDOM Web Components.
  4. Developers can hide the implementation details of their Custom Element and slot the content into the places needed without ShadowDOM.
  5. Live content projection as with ShadowDOM.
  6. Usage of slotchange event (optional).

Open questions

How can we signal that a LightDOM Web Component is using <slot>s without attaching another root node and appending the template to it?

A path forward

  1. Involve more people and gather more use-cases.
  2. Gain deeper insights from the working group and verify the case.
  3. Fleshing out how a flat-tree could look and be constructed.

Addendum

This is my first proposal that formed up after great talks with @bramus, @una, @fantasai, @andruud and @mirisuzanne at the CSSDay 2024.

Happy for any comments/corrections/recommendations, cheers!

Westbrook commented 5 months ago

I'd love to see your thoughts on how something like this could be resilient to DOM changes overtime, both from without the element AND within, rather than supporting what initially appears to be a one time stamp render merge of the initial content of the element and the "template" of the element.

Specifically, you use this.appendChild(...) in your example, which works a specific way that couldn't support this today. Do you see adding new arguments to the appendChild() method? Or do you see adding a new appendSomething() method to use alternatively? How would page with your custom element know how to interact with these updated DOM realities?

raphaelokon commented 5 months ago

Thanks for your questions @Westbrook

Do you see adding new arguments to the appendChild() method? Or do you see adding a new appendSomething() method to use alternatively?

The this.appendChild(...) used in the example is just a placeholder as we cannot answer how (and where) we would append the (updated) children, this is up for discussion I’d say. Either a new method like appendSomething() (or render() to imply that we re-render the template with updated children/slots maybe).

How would page with your custom element know how to interact with these updated DOM realities? This is also a good question. I think what is exposed to the page is either:

  1. … just the LightDOM children which are masked by a rendered composed tree of the template with the slotted content. I know that this sounds a lot like what ShadowDOM is already doing, but I imagine that in contrast to the ShadowDOM this tree has no boundary at all. It behaves almost like a dynamically templating.
  2. … a new way to construct a flat-tree where the children and the template /w slots are hooked into, but just the updated template being rendered/visible to the page similar to 1). /cc @andruud

In either way the page would have access to the LightDOM children of the custom element.

justinfagnani commented 1 month ago

This doesn't describe how slots work, particularly what host elements slots are associated with and what elements are slottable.

raphaelokon commented 1 month ago

@justinfagnani The most natural way for me: slots would be just a feature of the host element itself and attached to the it in a sort of new composed tree, without any shadow boundary. All slotted content would be projected into the new composed tree which would mask the LightDOM of the custom element. Regarding which elements are slottable I cannot think of any restrictions.

I know I am lacking a detailed description regarding slots in LightDOM, I just want to express the sentiment that a lot of authors I met during conferences and in my project work (main design system and large component libraries) suffer from the hard encapsulation that ShadowDOM poses onto. This is the main reason. Seeing #10808 empowers this albeit from the CSS side. Either way would be fine for authors I think.