Web components work in any framework or none, which makes them the native way to share UI widgets.
template holds inert markup: it's parsed but not rendered, scripts inside don't run and images don't load. Clone its .content with document.importNode or template.content.cloneNode(true) to stamp out copies, which is faster and safer than building HTML strings.
Custom elements let you register new tags. A class extending HTMLElement is registered with customElements.define("user-card", UserCard). Names must contain a hyphen, so they never clash with future HTML elements. Lifecycle callbacks run when the element is added (connectedCallback), removed (disconnectedCallback) or when an attribute listed in static observedAttributes changes (attributeChangedCallback).
Shadow DOM gives an element a private subtree: this.attachShadow({ mode: "open" }). Styles inside don't leak out and page styles don't leak in (inherited properties like font and custom properties still pass through). A <slot> in the shadow tree shows the element's light-DOM children; named slots (<slot name="title">) take children with a matching slot attribute.
You can also declare a shadow root in HTML with <template shadowrootmode="open">, so server-rendered components show up before any JavaScript runs.
Custom elements are upgraded when defined, so markup can appear before the script loads. Style the not-yet-defined state with the :not(:defined) selector to avoid a flash of unstyled content.
Example
<template id="card-tpl">
<style>.card { border: 1px solid #ccc; padding: 8px; border-radius: 8px; }</style>
<div class="card">
<strong><slot name="name">Anonymous</slot></strong>
<slot></slot>
</div>
</template>
<user-card><span slot="name">Ada</span> Wrote the first program.</user-card>
<script>
class UserCard extends HTMLElement {
connectedCallback() {
if (this.shadowRoot) return;
const tpl = document.getElementById("card-tpl");
this.attachShadow({ mode: "open" })
.append(tpl.content.cloneNode(true));
}
}
customElements.define("user-card", UserCard);
</script>Remember
- template content is parsed but inert until cloned
- Custom element names must contain a hyphen
- Shadow DOM scopes styles; slots project the element's children
- Declarative shadow DOM renders without JavaScript
Watch out for
- Expecting global CSS classes to style elements inside a shadow root.