Web Components
Web Components and Shadow DOM as a framework-agnostic encapsulation layer for micro-frontend composition.
You are an expert in using Web Components (Custom Elements, Shadow DOM, HTML Templates) as the integration layer for micro-frontend architectures. You help teams wrap framework-specific applications inside standards-based custom elements that compose cleanly regardless of the host technology.
## Key Points
- **Custom Elements** — `customElements.define()` registers a new HTML tag with lifecycle callbacks.
- **Shadow DOM** — an encapsulated DOM subtree with scoped styles.
- **HTML Templates / Slots** — declarative content projection.
- **Adopted Stylesheets** — performant, shareable CSS for shadow roots.
1. **Use `composed: true` on custom events** — without it, events dispatched inside Shadow DOM will not cross the shadow boundary and the shell will never hear them.
2. **Prefer properties over attributes for complex data** — attributes are always strings. For objects or arrays, expose a JavaScript property setter on the element.
3. **Use CSS custom properties for theming** — they are the intended mechanism for piercing Shadow DOM, enabling a shared design language without breaking encapsulation.
4. **Keep bundles self-contained** — each custom element script should bundle its own framework runtime (or share via import maps) so it works in any host.
5. **Register elements defensively** — check `customElements.get("mf-catalog")` before calling `define` to avoid errors when multiple scripts attempt registration.
6. **Leverage `adoptedStyleSheets`** — they are more performant than `<style>` tags and can be shared across multiple shadow roots.
- **Form participation** — custom elements with Shadow DOM do not natively participate in `<form>` submission. Use `ElementInternals` and `formAssociated` to restore form behavior.
- **SSR limitations** — Declarative Shadow DOM (`<template shadowrootmode="open">`) has growing support, but older browsers need a polyfill. Server-rendering shadow content is still maturing.
## Quick Example
```html
<!-- Usage -->
<mf-layout>
<mf-filter-panel slot="sidebar"></mf-filter-panel>
<mf-product-grid></mf-product-grid>
</mf-layout>
```skilldb get micro-frontend-skills/Web ComponentsFull skill: 286 linesWeb Components — Micro-Frontends
You are an expert in using Web Components (Custom Elements, Shadow DOM, HTML Templates) as the integration layer for micro-frontend architectures. You help teams wrap framework-specific applications inside standards-based custom elements that compose cleanly regardless of the host technology.
Overview
Web Components provide a browser-native mechanism for defining self-contained, reusable UI elements. In a micro-frontend context, each team packages their application or widget as a custom element. The shell application composes these elements in HTML without knowing or caring about the internal framework. Shadow DOM provides style encapsulation; custom events provide communication.
Key building blocks:
- Custom Elements —
customElements.define()registers a new HTML tag with lifecycle callbacks. - Shadow DOM — an encapsulated DOM subtree with scoped styles.
- HTML Templates / Slots — declarative content projection.
- Adopted Stylesheets — performant, shareable CSS for shadow roots.
Core Concepts
Custom Element Lifecycle
| Callback | When it fires |
|---|---|
constructor() | Element created or upgraded |
connectedCallback() | Element inserted into the document |
disconnectedCallback() | Element removed from the document |
attributeChangedCallback(name, oldVal, newVal) | An observed attribute changes |
These map directly to micro-frontend lifecycle needs: bootstrap in constructor, mount in connectedCallback, unmount in disconnectedCallback.
Style Encapsulation
Shadow DOM prevents styles from leaking in or out. This solves the global CSS collision problem that plagues other micro-frontend approaches. Styles defined inside the shadow root do not affect the outer document, and outer styles do not reach into the shadow root (except CSS custom properties, which intentionally pierce the boundary for theming).
Communication
Custom elements communicate outward via CustomEvent and inward via attributes/properties. This mirrors the props-down/events-up pattern familiar to component frameworks.
Implementation Patterns
Wrapping a React App in a Custom Element
// catalog-element.js
import React from "react";
import { createRoot } from "react-dom/client";
import CatalogApp from "./CatalogApp";
class CatalogElement extends HTMLElement {
static get observedAttributes() {
return ["category", "lang"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this._root = null;
}
connectedCallback() {
const mountPoint = document.createElement("div");
this.shadowRoot.appendChild(mountPoint);
this._root = createRoot(mountPoint);
this._render();
}
disconnectedCallback() {
if (this._root) {
this._root.unmount();
this._root = null;
}
}
attributeChangedCallback() {
this._render();
}
_render() {
if (!this._root) return;
this._root.render(
<CatalogApp
category={this.getAttribute("category")}
lang={this.getAttribute("lang") || "en"}
onProductSelect={(product) =>
this.dispatchEvent(
new CustomEvent("product-select", {
detail: product,
bubbles: true,
composed: true,
})
)
}
/>
);
}
}
customElements.define("mf-catalog", CatalogElement);
Wrapping a Vue App in a Custom Element
// checkout-element.js
import { createApp } from "vue";
import CheckoutApp from "./CheckoutApp.vue";
class CheckoutElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this._app = null;
}
connectedCallback() {
const mountPoint = document.createElement("div");
this.shadowRoot.appendChild(mountPoint);
this._app = createApp(CheckoutApp, {
cartId: this.getAttribute("cart-id"),
});
this._app.mount(mountPoint);
}
disconnectedCallback() {
if (this._app) {
this._app.unmount();
this._app = null;
}
}
}
customElements.define("mf-checkout", CheckoutElement);
Shell Application Composing Custom Elements
<!-- Shell app — no framework knowledge needed -->
<body>
<mf-navbar></mf-navbar>
<main>
<mf-catalog category="electronics" lang="en"></mf-catalog>
</main>
<script>
document.querySelector("mf-catalog")
.addEventListener("product-select", (e) => {
console.log("User selected:", e.detail);
// Navigate or update another micro-frontend
});
</script>
<script type="module" src="https://navbar.example.com/element.js"></script>
<script type="module" src="https://catalog.example.com/element.js"></script>
</body>
Shadow DOM with Adopted Stylesheets
// Shared design tokens via adopted stylesheets
const sharedTokens = new CSSStyleSheet();
sharedTokens.replaceSync(`
:host {
--color-primary: #0066cc;
--color-surface: #ffffff;
--font-family: system-ui, sans-serif;
--radius: 4px;
}
`);
class MyElement extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
const localStyles = new CSSStyleSheet();
localStyles.replaceSync(`
.card {
background: var(--color-surface);
border-radius: var(--radius);
font-family: var(--font-family);
padding: 16px;
}
`);
shadow.adoptedStyleSheets = [sharedTokens, localStyles];
}
}
Slot-Based Content Projection
// A layout wrapper micro-frontend
const template = document.createElement("template");
template.innerHTML = `
<style>
.layout { display: grid; grid-template-columns: 250px 1fr; gap: 16px; }
</style>
<div class="layout">
<aside><slot name="sidebar">Default sidebar</slot></aside>
<main><slot>Default content</slot></main>
</div>
`;
class LayoutElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define("mf-layout", LayoutElement);
<!-- Usage -->
<mf-layout>
<mf-filter-panel slot="sidebar"></mf-filter-panel>
<mf-product-grid></mf-product-grid>
</mf-layout>
Lazy-Loading Custom Element Definitions
// Only load the element bundle when the tag appears in the DOM
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const tag = entry.target.tagName.toLowerCase();
if (!customElements.get(tag)) {
const src = entry.target.dataset.src;
import(/* webpackIgnore: true */ src);
}
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll("[data-src]").forEach((el) => observer.observe(el));
Best Practices
- Use
composed: trueon custom events — without it, events dispatched inside Shadow DOM will not cross the shadow boundary and the shell will never hear them. - Prefer properties over attributes for complex data — attributes are always strings. For objects or arrays, expose a JavaScript property setter on the element.
- Use CSS custom properties for theming — they are the intended mechanism for piercing Shadow DOM, enabling a shared design language without breaking encapsulation.
- Keep bundles self-contained — each custom element script should bundle its own framework runtime (or share via import maps) so it works in any host.
- Register elements defensively — check
customElements.get("mf-catalog")before callingdefineto avoid errors when multiple scripts attempt registration. - Leverage
adoptedStyleSheets— they are more performant than<style>tags and can be shared across multiple shadow roots.
Common Pitfalls
- Form participation — custom elements with Shadow DOM do not natively participate in
<form>submission. UseElementInternalsandformAssociatedto restore form behavior. - SSR limitations — Declarative Shadow DOM (
<template shadowrootmode="open">) has growing support, but older browsers need a polyfill. Server-rendering shadow content is still maturing. - Event retargeting — events that cross shadow boundaries have their
targetretargeted to the host element. If you need the original target, useevent.composedPath(). - Framework hydration conflicts — mounting React/Vue inside a shadow root can confuse hydration if the server-rendered HTML does not match. Prefer client-only rendering inside custom elements.
- Memory leaks on disconnect — failing to unmount the inner framework in
disconnectedCallbackleaks the entire component tree. - Over-nesting shadow roots — deeply nested shadow DOMs make debugging painful and can hurt performance. Keep the nesting shallow.
Core Philosophy
Web Components are the browser-native solution to the micro-frontend composition problem. Custom elements let each team package their application as a self-contained HTML tag that the shell can compose declaratively, without knowing or caring about the internal framework. Shadow DOM provides the style encapsulation that prevents CSS collisions. Custom events provide the communication mechanism. These are standards, not library features — they work in every browser and every framework.
The lifecycle callbacks of custom elements map directly to micro-frontend lifecycle needs. connectedCallback is mount: create the React root, render the Vue app, or initialize the Angular component. disconnectedCallback is unmount: destroy the framework instance and clean up listeners. attributeChangedCallback is props update: re-render with new configuration. This mapping is clean, predictable, and requires no external orchestration framework.
CSS custom properties are the intended mechanism for theming across shadow boundaries. They are the only CSS values that pierce the shadow DOM by design, making them perfect for shared design tokens. Define your design system as CSS custom properties on :root, and every custom element — regardless of its internal framework — can reference them for consistent styling without breaking encapsulation.
Anti-Patterns
-
Forgetting
composed: trueon custom events — events dispatched inside Shadow DOM do not cross the shadow boundary by default; withoutcomposed: true, the shell never receives them. -
Passing complex data through HTML attributes — attributes are always strings; for objects, arrays, or functions, expose JavaScript property setters on the element instead of serializing to attribute strings.
-
Not checking
customElements.get()beforedefine()— if multiple scripts attempt to define the same custom element tag, the second call throws; always check for existing registration first. -
Failing to unmount the inner framework in
disconnectedCallback— removing the custom element from the DOM without unmounting React, Vue, or Angular inside it leaks the entire component tree and its associated memory. -
Deeply nesting shadow roots — each level of Shadow DOM nesting adds debugging complexity and performance overhead; keep the shadow root hierarchy shallow and use composition via slots rather than nested custom elements.
Install this skill directly: skilldb add micro-frontend-skills
Related Skills
Deployment
Independent deployment patterns for micro-frontends including CI/CD pipelines, versioning, rollback, and environment strategies.
Design System Sharing
Strategies for sharing design systems, component libraries, and visual consistency across independently deployed micro-frontends.
Iframe Composition
iFrame-based micro-frontend composition for maximum isolation between independently deployed frontend applications.
Module Federation
Webpack Module Federation for sharing code and dependencies across independently deployed micro-frontends at runtime.
Routing
Cross-application routing strategies for micro-frontends including shell-controlled routing, distributed routing, and URL contracts.
Shared State
Cross-application state sharing patterns for micro-frontends including event buses, shared stores, and URL-based state.