11. CSS - attachShadow

Finally, we no longer have to worry about naming collisions and overwriting other code when using ES Modules. We can use generic class names like Button or Note without adding extra noise to our code, making it much more enjoyable to read and write. However, is there a similar solution for CSS, like CSS Modules? Unfortunately, no. Nevertheless, the W3C seems to be working on it.

This is unfortunate, as we have found a way to encapsulate JavaScript code from other widgets, but our CSS still lacks this capability. However, there is a solution: we can style elements programmatically within JavaScript. While you can style elements using element.style.propertyName, a better approach is to use Web Components with the Shadow DOM.

If we modify the code of our Note widget to utilize the Shadow DOM, it would look something like this:

class Note {
	constructor({ x = 100, y = 100, message = "New note" }) {
        this.setThisContext();

        const handleBarEl = document.createElement("div");
        handleBarEl.classList.add("handle-bar");
        handleBarEl.addEventListener("mousedown", this.startDragHandler);

        this.messageEl = document.createElement("textarea");
        this.messageEl.classList.add("message");
        this.messageEl.value = message;

        const sheet = new CSSStyleSheet();
        sheet.replaceSync(`
			:host {
				background-color: lightgoldenrodyellow;
				display: flex;
				flex-direction: column;
                position: fixed;
                left: calc(var(--x) * 1px);
                top: calc(var(--y) * 1px);
                width: 250px;
                height: 250px;
            }

            .handle-bar {
                background-color: var(--handle-bar-color, tomato);
                cursor: pointer;
                flex: 0 0 20px;
            }

            .message {
                flex: 1 0;
                outline: none;
                padding: 8px;
                resize: none;
            }
        `);

        this.rootEl = document.createElement("div");

        const shadow = this.rootEl.attachShadow({ mode: "open" });
        shadow.append(handleBarEl, this.messageEl);
        shadow.adoptedStyleSheets.push(sheet);

        this.updatePosition(x, y);
    }
}

In this code, we use CSSStyleSheet and call the replaceSync() method to set the CSS. As you can see, we use very generic class names like .message. This is the power of the Shadow DOM; we don't need to use selectors like .wem-academy-note > .message to avoid collisions with other selectors.

The final step is to create the Shadow DOM and append our CSSStyleSheet instance to it. We accomplish this with the attachShadow() method available on an HTMLElement. This method returns a ShadowRoot, which is a special type of DOM node but provides the familiar DOM manipulation methods, such as shadow.append(handleBarEl, this.messageEl), which we use to append the handle bar and message box. Next, we use shadow.adoptedStyleSheets.push(sheet) to attach the CSS to this Shadow DOM.

That's about it! You can see that there are several ways to style elements, each with its pros and cons. It is up to you to choose the one that best fits your needs. Hopefully, the W3C will expedite the development of CSS Modules, as that would seem like the optimal solution.

Last updated

Was this helpful?