Block shadow DOM rendering with linked styles

Viewed 836

Shadow DOM supports the use of <link> tags to load styles that are scoped in the same way as a style declared with <style> which is very convenient but comes with the issue that styles are applied only when they are ready and while the styles are being loaded there is FOUC that can't be prevented with the :defined pseudo selector of custom elements for example. Another problem I have encountered is measuring elements inside the shadow root when the custom element gets constructed or connected because the "real" dimensions are known after the style sheet is loaded and applied, something that I don't know when will happen(maybe the ResizeObserver will help when implemented?)
Can anyone think of a clever way to get around those issues(without inlining the styles manually or with a build step)? Does my concern make sense? Can it be seen as a bug given that <link rel="stylesheet> in the <head> block rendering and this feature should be analogous to that?

2 Answers

If you want to avoid FOUC, you should hide the element until the style is applied. You can know when your style is loaded when you use fetch or XMLHttpRequest, or <link onload=...>.

About the dimension issue, it's not specific to Shadow DOM but a consequence of the CSS architecture.

Anyway, it is often recommended to set the with and height of an element to avoid FOUC but also to avoid full page redraw and so to speed up rendering.

Note that <link rel="stylsheet"> support in Shadow DOM is very new, so maybe it will not work as expected in all browsers.

Here is some code that created a component with shadowDOM. For simplicity I am emulating a long load time for the CSS by using setTimeout. After 1 second I apply the CSS into the element.

As you stated the element looks one way until the CSS is loaded.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>FOUC Prevention for WC</title>
    <script>
    var delayedStyles = document.createElement('style');
    delayedStyles.textContent = `
    :host {
      background-color: #DEE;
      border: 1px solid #999;
      display: block;
      width: 400px;
    }

    h1 {
      color: green;
      font: 18px/1em Tahoma;
      padding: 3px 6px;
    }

    p {
      margin: 5px 10px;
      padding: 10px;
    }
    `;
    var template = document.createElement('div');
    template.innerHTML = `
    <h1>The header</h1>
    <p>Some body content</p>
    `;

    // Class for `<my-component>`
    class MyComponent extends HTMLElement {
      constructor() {
        super();

        var sr = this.attachShadow({mode: 'open'});
        setTimeout(() => sr.appendChild(delayedStyles.cloneNode(true)), 1000);


        sr.appendChild(template.cloneNode(true));
      }
    }

    // Define our web component
    customElements.define('my-component', MyComponent);
    </script>
  </head>
  <body>
    <my-component></my-component>
  </body>
</html>

With a minor change we can hide the element until the CSS loads:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>FOUC Prevention for WC</title>
    <script>
    var delayedStyles = document.createElement('style');
    delayedStyles.textContent = `
    :host {
      background-color: #DEE;
      border: 1px solid #999;
      display: block;
      width: 400px;
    }

    .innerShell {
      display: block !important;
    }

    h1 {
      color: green;
      font: 18px/1em Tahoma;
      padding: 3px 6px;
    }

    p {
      margin: 5px 10px;
      padding: 10px;
    }
    `;
    var template = document.createElement('div');
    template.setAttribute('style', 'display: none;');
    template.className = 'innerShell';
    template.innerHTML = `
    <h1>The header</h1>
    <p>Some body content</p>
    `;

    // Class for `<my-component>`
    class MyComponent extends HTMLElement {
      constructor() {
        super();

        var sr = this.attachShadow({mode: 'open'});
        setTimeout(() => sr.appendChild(delayedStyles.cloneNode(true)), 1000);


        sr.appendChild(template.cloneNode(true));
      }
    }

    // Define our web component
    customElements.define('my-component', MyComponent);
    </script>
  </head>
  <body>
    <my-component></my-component>
  </body>
</html>

To do this I set the style tag of my top-level element in my shadow DOM. I set it to display: none; This hides the inner content of the shadow DOM.

Then, 1 second later, when the CSS is loaded, it overrides the display: none; with display: block !important. I have to use !important to become more specific then the css set in the style tag.

Only after the CSS loads does my element becomes visible.

As another options You could also place an onload event handler on your <link> tag:

<link rel="stylesheet" href="mystylesheet.css" onload="sheetLoaded()" onerror="sheetError()">

Which would let you know, in code, that it was loaded and then just remove the style attribute.

Related