The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Declare a template<template id="card-template"> <article> <slot> </slot> </article> </template>View examples
Clone template contentconst fragment = template.content.cloneNode(true)View examples
Register an elementcustomElements.define('status-card', StatusCard)View examples
Handle connectionconnectedCallback() { this.render(); }View examples
Observe attributesstatic observedAttributes = ['status']View examples
React to an attributeattributeChangedCallback(name, oldValue, newValue) { this.render(); }View examples
Attach an open shadow rootconst 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 registrationawait 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

01

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.

Clone and populate a card
<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>
Back to quick reference ↑
02

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.

Upgrade a status card
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);
Back to quick reference ↑
03

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.

Observe a status attribute
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);
Back to quick reference ↑
04

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.

Attach a small shadow tree
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);
Back to quick reference ↑
05

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.

Named action slot and style part
<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>
Back to quick reference ↑

Local code tester

Try a custom element

Edit a progressively enhanced element with a shadow root and projected content.

Runs in your browser
Preview

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. WHATWGHTML: The template elementhtml.spec.whatwg.org
  2. WHATWGHTML: Custom elementshtml.spec.whatwg.org
  3. WHATWGDOM: Shadow treesdom.spec.whatwg.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback