Westbrook Johnson

thinks he might

write more:

someday,

here.

In my day-to-day work, I tend to write custom elements using the LitElement base class. This is great because it reduces boilerplate, focuses me on the functionality I'm delivering, and enforces at least a baseline normalization of patterns with my coworkers. However, I at least put a passing attempt at writing vanilla web components when bringing together explainers and conversation around non-Lit contexts; see articles on the styling of basic custom elements or the internals of custom elements. That process often leaves me to think out loud about the patterns that might be useful for leveraging when building many/shared/distributed custom elements with vanilla JS, here's one...

Adopting your parent's style

First published: 2022-11-27

Modified: 2023-12-30

After much adieu, Constructible Stylesheets are finally about to land in all modern browsers. This means so much for tools that have already adopted them in concert with fallbacks to previous techniques to support less forward-thinking browsers; both in form of performance and code simplicity. For tools that haven't adopted them yet, finally, there is a clear path to the performance benefits that come along with this new browser capability. It also means that there is a new possibility of finding new and interesting use cases only made available to us by the presence of this API, thanks to its broader availability. One of those includes the ability to adopt your parent's styles, which might work like this:

import styles from './styles.css' with { type: 'css' };

class ParentStylesAdoptingElement extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open'});
        this.shadowRoot.appendChild(
            Test.template()
        );
    }

    shadowRoot;

    static template;

    static {
        const template = document.createElement('template');
        template.innerHTML = /* html */`
            <slot></slot>
        `;
        this.template = () => template.content.cloneNode(true);
    }

    connectedCallback() {
        const hostStyles = this.getRootNode().adoptedStyleSheets || [];
        this.shadowRoot.adoptedStyleSheets = [
            ...hostStyles,
            styles
        ];
    }
}

customElements.define('parent-styles-adopting-element', ParentStylesAdoptingElement);

This example also uses static blocks, but they aren't specifically doing any new or interesting work in this case.

What's doing the interesting work in the connectedCallback(). It finds the root node of the host and then takes any adoptedStyleSheets it may have been provided and applies them to the host elements shadow root, as well.

connectedCallback() {
    const hostStyles = this.getRootNode().adoptedStyleSheets || [];
    this.shadowRoot.adoptedStyleSheets = [
        ...hostStyles,
        styles
    ];
}

In this way, we've created a custom element with a shadow root that for all intents and purposes no longer encapsulates its shadow DOM from styles in its parent DOM tree. Could this be a solution to "open-stylable" Shadow Roots?

Well, not a total solution... caveats incoming!

In a world where styles are applied to an HTML document with HTML (gasp!) the ceiling of the parent style adoption chain would likely be maintained by the first custom element on a page. That is to say <link rel="style" href="styles.css"> doesn't get registered on the adoptedStyleSheets array. For shadow roots, it is more common to apply styles via adoptedStyleSheets as before Declarative Shadow DOM (DSD) there was no way to attach a shadow root to an element without JS, but with the growth of DSD, this context, like the host HTML document, will "suffer" from the lack of documents applying their styles in this way.

Much of the above involves changes to browser specifications, which may prove time or cost prohibitive. However, the idea of shared, or encapsulated, or componentized styles will continue to be with us for quite some time. That means that we'll be better off with more flexible patterns for consuming them rather than less. adoptedStyleSheets will be a major force in opening those up, especially in concert with import attributes, and the more we can talk about the possibilities of these new APIs, the better!


Editors note: some of the APIs outlined in the examples above are not yet available cross-browser. While these are the target APIs that browsers have agreed to, in order to support cross-browser delivery of this article, live demo code is modified from the example to work fully cross-browser.