Declarative Shadow DOM


#1

Currently, the only way to construct a ShadowRoot is via the imperative attachShadow API.

Has any consideration been given to providing a declarative Shadow DOM tag?

In order to support Web Component server side rendering, there has to be a way to serialize their contents in a way that can be displayed before JS is initialized. This approach is possible with React components and CSS module implementations. But it would be great if all these CSS module implementations could just be using shadow in the future.

<style>
  .btn {
    color: blue;
  }
</style>
<button class="btn">Blue Button</button>

<shadow-root mode="open">
  <style>
    .btn {
      color: red;
    }
  </style>

  <button class="btn">Red Button</button>
</shadow-root>

Ironic polyfill implemented via Custom Elements.

<script>
  class ShadowRootElement extends HTMLElement {
    constructor() {
      super();
      this.root = this.attachShadow({mode: this.getAttribute('mode')});
    }
    connectedCallback() {
      for (const child of Array.from(this.childNodes)) {
        this.root.appendChild(child);
      }
    }
  }
  customElements.define('shadow-root', ShadowRootElement);
</script>

Obviously this polyfill has a number of problems and would be better served with a native implementation that could run before scripts are loaded.

Now that <style scoped> has been removed, this provides that same declarative mechanism.

Also related, I’ve found hints of HTMLShadowElement in Shadow DOM v0, however it don’t think it could ever be initialized via a tag name.


#2

Can you explain why these server-rendering solutions are able to insert <shadowroot> elements but are not able to insert <script>document.querySelector("...").attachShadow().appendChild(document.querySelector("..."));</script>?


#3

CSP often limits inline scripting usage. Or if scripting is disabled entirely by the UA.

It seems like there are other HTML parsing performance benefits for a browser to render markup as its coming in without having to context switch between hundreds of blocking inline <script> tags on the page.


#4

CSP can be configured to allow select (known ahead of time) inline scripts. We should not be adding new elements to work around CSP.

I don’t think making performance conjectures without data is very helpful here.


#5

You never know how exactly scripts are blocked by a script blocker.


#6

Yes, just like you never know what elements are blocked by a script blocker either. If you introduce nonstandard ways of messing with your page’s contents into the mix, all bets are off. Inventing new elements to get around such blockers is not likely to work for long.


#7

It’s not about working around script blockers, it’s about making those able to do their job without inevitably and unreasonably breaking other parts of document that can be expressed via nonscript means. Script blockers don’t block CSS or HTML since this is not what those are intended for, not because those couldn’t technically.

Even if a standardized Shadow-DOM-related subset of JS would be defined by a spec, most script blockers would most likely keep blocking JS completely, including (and regardless of) the subset formally called “safe” in the spec.


#8

I created a gist that does a performance comparison between using the inline script technique that @domenic suggested vs. normal inlined HTML: https://gist.github.com/matthewp/f68d892ceb54ccc44f42691b41638f12

This creates 1000 divs, and in the shadow DOM case it moves the HTML from a <template> into the div’s shadow root using an inline script.

On my local machine I get about 4ms load time in the inlined HTML version and ~200ms in the shadow DOM version in Chrome Canary. Safari 10.1 does a bit better at ~4ms for the inlined and ~50ms for the shadow DOM version.

I draw no conclusions from this, just presenting the data. A <shadow-root> element might have a similar cost.

Happy to improve the benchmark if there are any suggestions! I tried using parentNode/previousElementSibling instead of getElementById but that didn’t change the numbers at all.


#9

The other use case is for frameworks that have declarative templating, e.g. GlimmerJS. Being able to define your shadow dom in the template is powerful and clear at the same time.


#10

It isn’t necessarily about server-side rendering. It could be about language. Someone can write the shadow-root element in HTML similar to how they can write a new block scope in JavaScript in order to introduce a new scope for variables. By writing a <shadow-root> element then placing style and elements inside, a scope can be created for CSS isolation when that is all that is desired (for example) without needing JavaScript. It can be a sort of convenience as far as writing HTML goes, which is a much different use case compared to using shadow dom to define component internals. Plus, having JS-only API means that some CSS features are disabled if JS is disabled (if we can’t create shadow roots with JS, then we can’t have CSS scope).


#11

Also writing that in server code is much uglier too.


#12

Not completely to grips with the use-case here. You don’t use Javascript at all? Deferred till condition X? Just want to speed up query selectors?

Anyway do <SLOT> elements not help?


#13

I think it is a huge benefit to be able to write CSS isolation in HTML without requiring JS. This doesn’t sound right: if JS is turned off, then we loose the feature of CSS isolation. If it was supported in HTML, then jS can be turned off, and CSS isolation can still work. That’s a pretty worth reason I think.

<style>
  /* ... some global styles ...*/
</style>
<body>
  <div>Some globally-styled content</div>
  <shadow-root>
    <style>
      /* scoped style without JavaScript */
    </style>
    <div> Some content with scoped-style. The style doesn't affect external content! </div>
  </shadow-root>
<body>

That is plain and simple, and I think JavaScript should not be required to achieve this.

EDIT: but things may get tricky, for exampe someone might be inclined to write multiple shadow-roots to create multiple areas with scoped styling (note that multiple shadow roots are not allowed):

<style>
  /* ... some global styles ...*/
</style>
<body>
  <div>Some globally-styled content</div>
  <shadow-root>
    <style>
      /* scoped style without JavaScript */
    </style>
    <div> Some content with scoped-style. The style doesn't affect external content! </div>
  </shadow-root>
  <shadow-root>
    <style>
      /* other scoped style without JavaScript */
    </style>
    <div> Some other content with scoped-style. The style doesn't affect external content! </div>
  </shadow-root>
<body>

In this case the solution may be to use wrappers around the shadow-roots:

<style>
  /* ... some global styles ...*/
</style>
<body>
  <div>Some globally-styled content</div>

  <div>
  <shadow-root>
    <style>
      /* scoped style without JavaScript */
    </style>
    <div> Some content with scoped-style. The style doesn't affect external content! </div>
  </shadow-root>
  </div>

  <div>
  <shadow-root>
    <style>
      /* other scoped style without JavaScript */
    </style>
    <div> Some other content with scoped-style. The style doesn't affect external content! </div>
  </shadow-root>
  </div>
<body>

#14

Hi Joseph,

Sorry to be slow on the uptake but can’t this simply be achieved with another CSS class? Or perhaps just prefixing your selectors with " #myHost

"?

Perhaps if you explained what was to be gained by this approach? Especially, given the semi-isolation described here: -

Anyway I’m banned from WICG so good-luck.

Cheers Richard


#15

Yeah, those are the currently-existing techniques, which Less, Sass, etc, use when they compile to CSS. But it isn’t entirely full proof: you can’t guarantee that someone won’t in the future also write #myHost or some other colliding class name (in a big project for example). Also it takes effort to write collision-free names.

CSS scoping is programmatically encapsulated, unlike just following conventions. With CSS scope, it means someone can write a class button without having to write component-subcomponent-button or something else more complicated to just to avoid collision…

Because with scoping the collision is avoided by the program, not by writing conventions. For example, look how easy it is to style scoped button without having to write extra stuff:

<shadow-dom>
  <style>
    button {
      color: red
    }
  </style>
  <button></button>
</shadow-dom>

#16

I tried to aggregate discussions from here and https://github.com/whatwg/dom/issues/510, to a slightly more structured proposal at https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Declarative-Shadow-DOM.md I would really appreciate any feedback.


#17

I like the proposal, but maybe it needs a slightly special syntax to disambiguate from normal DOM elements. For example:

    <#shadowroot mode="open">
        <h2>Shadow Content</h2>
        <slot></slot>
    </#shadowroot>

with the leading # so that it makes the distinction clear. But, then again, <#foo> isn’t a backwards-compatible format, and in older browsers the <#foo> will appear as text in the rendering. But even with back0compat syntax, it will lead to content not rendered as expected in older browsers. So it’s tricky!


#18

Thanks for the review!

I’m afraid that so different syntax would be problematic for parser changes, polyfilling, etc. It does not fit the current definition of an element name https://w3c.github.io/html-reference/syntax.html#tag-name. However, from a users perspective, I don’t see much disadvantages.

We already have not-“normal DOM elements”, the elements with different parsing contexts, like: template, script, tr. So far, to distinct the special behavior, we didn’t use any mean other than the tag name.

At the end it will still be part of the DOM, why should we disambiguate DOM from DOM? Is that this “disappearing” behavior that makes you think it may be that special and confusing?


#19

Well, what happens if we do

document.querySelector('shadowroot').remove()

Does that remove the shadowroot even though that is not supposed to be possible?

Does the parser remove the <shadowroot> element during parsing, so that the result is a real shadow root but the DOM no longer contains a <shadowroot> element? If not, then is it just an inert element after parsing?

If we add children to <shadowroot>, do they somehow get distributed into the content of the actual root?

Is a reference to a <shadowroot> the same as an actual shadow root? If not, then how can elements exist both as content of a <shadowroot> and content of the actual real shadow root? Do we introduce another distribution mechanism just for distributing elements from <shadowroot> to actual shadow root?

What happens if I move the <shadowroot> to another parent?

If it is a normal element, does that mean that <shadowroot> can be distributed into a… shadow root?

What happens if we add more than one <shadowroot> element to the parent element?

<shadowroot> is so different than all other elements in the sense that shadow roots are meant for composing HTMLElements/DOM, and currently they aren’t HTML elements. Making them HTML elements has many implications (which may not be desired).

This is why I thought maybe an alternate syntax would let us sidestep all implications, and specify the behavior from scratch. For example, maybe the <#shadowroot> element is ephemeral, it only exists in your markup, and after being parsed it will not be in the DOM, but the parent element will have a shadow root added to it. That way, if it isn’t in the DOM, it isn’t queryable, isn’t removable, isn’t appendable, can’t have children because it doesn’t exist, can’t be distributed because it doesn’t exist, etc.

But then, what if one changes innerHTML of an element, introducing a new <#shadowroot> element? Maybe nothing should happen. It just disappears, and the previous root shadow root that was created from the previous parsing remains.


For me, the main use case for <shadowroot> is in server-side rendering, so that the client can know how to re-create the composition of trees from the HTML received from the server.