The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Declare a template | <template id="card-template">
<article>
<slot>
</slot>
</article>
</template> | View examples |
| Clone template content | const fragment = template.content.cloneNode(true) | View examples |
| Register an element | customElements.define('status-card', StatusCard) | View examples |
| Handle connection | connectedCallback() { this.render(); } | View examples |
| Observe attributes | static observedAttributes = ['status'] | View examples |
| React to an attribute | attributeChangedCallback(name, oldValue, newValue) { this.render(); } | View examples |
| Attach an open shadow root | const root = this.attachShadow({ mode: 'open' }) | View examples |
| Project default content | <slot>Fallback text</slot> | View examples |
| Project named content | <slot name="actions"></slot> | View examples |
| Expose a style part | <button part="button"><slot></slot></button> | View examples |
| Wait for registration | await customElements.whenDefined('status-card') | View examples |
Templates hold inert DOM for later cloning; custom elements attach behavior to valid hyphenated tag names; shadow trees provide an encapsulated subtree; slots project light-DOM content into that subtree. Keep component APIs small, preserve document semantics, and progressively enhance content that remains useful before upgrade.
Step by step
Detailed examples
Clone inert template content deliberately
A template's children live in its content DocumentFragment and do not render or run scripts while stored there. Clone before insertion because appending the fragment moves its nodes. Populate cloned nodes with textContent or DOM APIs rather than unsafe HTML interpolation.
<template id="person-template"><article><h2></h2><p></p></article></template>
<div id="people"></div>
<script>
const fragment = document.querySelector('#person-template').content.cloneNode(true);
fragment.querySelector('h2').textContent = 'Ada';
fragment.querySelector('p').textContent = 'Maintainer';
document.querySelector('#people').append(fragment);
</script> Register components once with valid names
Autonomous custom-element names must contain a hyphen and must not use reserved names. The constructor should initialize internal state without assuming children are available; connectedCallback is a better place for document-dependent work. Guard registration in environments that may load a bundle twice.
class StatusCard extends HTMLElement {
connectedCallback() {
if (!this.querySelector('strong')) {
const label = document.createElement('strong');
label.textContent = this.getAttribute('status') ?? 'unknown';
this.append(label);
}
}
}
if (!customElements.get('status-card')) customElements.define('status-card', StatusCard); Reflect only meaningful public state
observedAttributes limits attribute callbacks to declared names. Attribute values are strings or null, so parse and validate them. Avoid cycles when reflecting properties back to attributes, and remove global listeners in disconnectedCallback so detached components do not leak behavior.
class StatusBadge extends HTMLElement {
static observedAttributes = ['status'];
connectedCallback() { this.render(); }
attributeChangedCallback() { if (this.isConnected) this.render(); }
render() { this.textContent = this.getAttribute('status') ?? 'unknown'; }
}
customElements.define('status-badge', StatusBadge); Use shadow DOM for encapsulation, not secrecy
Shadow DOM scopes selectors and creates a separate tree for events and accessibility composition. Open mode exposes shadowRoot for tooling and integration; closed mode is not a security boundary. External document styles do not normally select internal nodes, but inherited properties and custom properties still cross the boundary.
class InfoNote extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = '<style>:host{display:block}aside{border-inline-start:4px solid #2563eb;padding:1rem}</style><aside><slot></slot></aside>';
}
}
customElements.define('info-note', InfoNote); Project content and expose intentional styling hooks
Slots render light-DOM children at defined insertion points; unassigned content is not automatically visible when named slots are used. Fallback slot content appears only when nothing is assigned. part exposes specific internal elements without forfeiting all encapsulation, while custom properties are useful for value-level theming.
<template id="panel-template">
<style>.panel{border:1px solid;padding:1rem}</style>
<section class="panel" part="panel"><slot></slot><footer><slot name="actions">No actions</slot></footer></section>
</template>
<action-panel>Content <button slot="actions">Save</button></action-panel>
<style>action-panel::part(panel) { border-radius: .75rem; }</style> Local code tester
Try a custom element
Edit a progressively enhanced element with a shadow root and projected content.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



