Home

Web Components Cheat Sheet

Web Components Cheat Sheet

A practical reference for building native Web Components based on real-world patterns.


Core Structure

Basic Component Template

 1class MyComponent extends HTMLElement {
 2  constructor() {
 3    super(); // Always call super() first
 4    this.attachShadow({ mode: "open" }); // Shadow DOM encapsulation
 5    // Initialize state here
 6    this.isActive = false;
 7  }
 8
 9  // Lifecycle: element added to DOM
10  connectedCallback() {
11    this.render();
12    this.setupEventListeners();
13  }
14
15  // Lifecycle: element removed from DOM
16  disconnectedCallback() {
17    // Clean up event listeners, timers, etc.
18  }
19
20  // Lifecycle: element moved to new document
21  adoptedCallback() {
22    // Called when moved to a new document (e.g., iframe)
23  }
24
25  // Lifecycle: observed attributes changed
26  attributeChangedCallback(name, oldValue, newValue) {
27    if (oldValue !== newValue) {
28      this.render();
29    }
30  }
31
32  // Declare which attributes to observe
33  static get observedAttributes() {
34    return ["active", "size"];
35  }
36}
37
38// Register the component
39customElements.define("my-component", MyComponent);

Key Gotcha: Constructor Restrictions

 1// WRONG - Cannot access attributes in constructor
 2constructor() {
 3  super();
 4  this.value = this.getAttribute('value'); // Returns null!
 5  // Cannot access parentElement, children, or most DOM features
 6}
 7
 8// CORRECT - Access attributes in connectedCallback
 9connectedCallback() {
10  this.value = this.getAttribute('value'); // Works correctly
11  this.render();
12}

Why? The constructor runs before the element is added to the DOM and before attributes are set. Use connectedCallback() for DOM interactions and attribute access.


Attributes & Properties

Observed Attributes Pattern

 1// Declare which attributes trigger updates
 2static get observedAttributes() {
 3  return ['position', 'width', 'active'];
 4}
 5
 6// Called when observed attributes change
 7attributeChangedCallback(name, oldValue, newValue) {
 8  if (oldValue !== newValue) {
 9    this.render(); // Re-render on change
10  }
11}
12
13// Convenience getters with defaults
14get position() {
15  return this.getAttribute('position') || 'left';
16}
17
18get width() {
19  return this.getAttribute('width') || '280px';
20}
21
22// Setters for programmatic control
23set width(value) {
24  this.setAttribute('width', value);
25}

Boolean Attributes

 1// Boolean attributes check presence, not value
 2get active() {
 3  return this.hasAttribute('active'); // true if present
 4}
 5
 6set active(value) {
 7  if (value) {
 8    this.setAttribute('active', ''); // Set empty string
 9  } else {
10    this.removeAttribute('active');
11  }
12}

Attribute Naming Convention

1// HTML uses kebab-case
2<my-component action-bar-width="64px"></my-component>
3
4// Convert to camelCase in JavaScript
5get actionBarWidth() {
6  return this.getAttribute('action-bar-width') || '64px';
7}

Reflecting Properties to Attributes

 1// Keep properties and attributes in sync
 2class MyComponent extends HTMLElement {
 3  get value() {
 4    return this.getAttribute("value") || "";
 5  }
 6
 7  set value(val) {
 8    // Reflect property changes to attributes
 9    if (val) {
10      this.setAttribute("value", val);
11    } else {
12      this.removeAttribute("value");
13    }
14  }
15}

Shadow DOM & Styling

Shadow DOM Modes

1// Open mode - accessible via element.shadowRoot
2this.attachShadow({ mode: "open" });
3
4// Closed mode - shadowRoot is null (rarely used)
5// this.attachShadow({ mode: 'closed' });
6// Note: Use 'open' in most cases for better debuggability

Template Literal Rendering

 1render() {
 2  this.shadowRoot.innerHTML = `
 3    <style>
 4      /* Scoped styles - won't leak outside component */
 5      :host {
 6        display: block;
 7        --custom-property: #ffffff;
 8      }
 9
10      button {
11        color: var(--custom-property);
12      }
13    </style>
14
15    <button>Click me</button>
16  `;
17}

Using HTML Template Elements

 1class MyComponent extends HTMLElement {
 2  constructor() {
 3    super();
 4    this.attachShadow({ mode: 'open' });
 5
 6    // Create template once (can be reused)
 7    const template = document.createElement('template');
 8    template.innerHTML = `
 9      <style>
10        :host { display: block; }
11      </style>
12      <div class="container">
13        <slot></slot>
14      </div>
15    `;
16
17    // Clone and append template content
18    this.shadowRoot.appendChild(template.content.cloneNode(true));
19  }
20}
21
22// Alternative: Define template in HTML
23<template id="my-template">
24  <style>:host { display: block; }</style>
25  <div><slot></slot></div>
26</template>
27
28<script>
29class MyComponent extends HTMLElement {
30  constructor() {
31    super();
32    const template = document.getElementById('my-template');
33    const content = template.content.cloneNode(true);
34    this.attachShadow({ mode: 'open' }).appendChild(content);
35  }
36}
37</script>

CSS Custom Properties (Theming)

 1render() {
 2  const width = this.width; // Get attribute value
 3
 4  this.shadowRoot.innerHTML = `
 5    <style>
 6      :host {
 7        /* Define defaults, allow external override */
 8        --sidebar-width: ${width};
 9        --bg-color: var(--custom-bg, #ffffff); /* Fallback chain */
10      }
11
12      aside {
13        width: var(--sidebar-width);
14        background: var(--bg-color);
15      }
16    </style>
17  `;
18}
1/* External stylesheet can override custom properties */
2my-component {
3  --custom-bg: #f0f0f0;
4  --sidebar-width: 320px;
5}

:host Pseudo-Class Patterns

 1:host {
 2  /* Styles the component itself */
 3  display: block;
 4  position: relative;
 5}
 6
 7:host([hidden]) {
 8  /* Style based on attributes */
 9  display: none;
10}
11
12:host(.active) {
13  /* Style based on classes */
14  border: 2px solid blue;
15}
16
17:host(:hover) {
18  /* Pseudo-classes work too */
19  opacity: 0.8;
20}
21
22:host-context(.dark-theme) {
23  /* Style based on ancestor classes */
24  --bg-color: #333;
25}

Dynamic Styles with Template Literals

 1render() {
 2  const isLeft = this.position === 'left';
 3
 4  this.shadowRoot.innerHTML = `
 5    <style>
 6      .sidebar {
 7        ${isLeft ? 'left: 0' : 'right: 0'};
 8        box-shadow: ${isLeft ? '2px 0 8px' : '-2px 0 8px'} rgba(0, 0, 0, 0.1);
 9      }
10    </style>
11  `;
12}

Constructable Stylesheets (Modern Approach)

 1// Better performance for repeated styles
 2class MyComponent extends HTMLElement {
 3  static styles = new CSSStyleSheet();
 4
 5  static {
 6    this.styles.replaceSync(`
 7      :host { display: block; }
 8      button { padding: 8px 16px; }
 9    `);
10  }
11
12  constructor() {
13    super();
14    this.attachShadow({ mode: "open" });
15    // Adopt shared stylesheet
16    this.shadowRoot.adoptedStyleSheets = [MyComponent.styles];
17  }
18}

Slots & Content Projection

Named Slots Pattern

 1// Component template
 2render() {
 3  this.shadowRoot.innerHTML = `
 4    <div class="header">
 5      <slot name="header">Default header</slot>
 6    </div>
 7
 8    <div class="content">
 9      <slot></slot> <!-- Default/unnamed slot -->
10    </div>
11
12    <div class="footer">
13      <slot name="footer"></slot>
14    </div>
15  `;
16}
1<!-- Usage -->
2<my-component>
3  <h1 slot="header">Custom Header</h1>
4  <p>This goes in the default slot</p>
5  <footer slot="footer">Custom Footer</footer>
6</my-component>

Styling Slotted Content

 1/* Style content projected into slots */
 2::slotted(*) {
 3  margin: 0;
 4}
 5
 6::slotted(svg) {
 7  width: 20px;
 8  height: 20px;
 9  fill: currentColor;
10}
11
12::slotted(h1) {
13  font-size: 2rem;
14}

Gotcha: ::slotted() Limitations

 1/* WRONG: Cannot style descendants of slotted content */
 2::slotted(div p) {
 3}
 4
 5// Won't work
 6
 7/* WRONG: Cannot use combinators */
 8::slotted(*) + ::slotted(*) { } // Won't work
 9
10/* CORRECT: Only direct slotted elements */
11::slotted(*) {
12}
13::slotted(.class-name) {
14}
15::slotted([attribute]) {
16}

Detecting Slotted Content

 1connectedCallback() {
 2  const slot = this.shadowRoot.querySelector('slot[name="header"]');
 3
 4  slot.addEventListener('slotchange', (e) => {
 5    const nodes = slot.assignedNodes();
 6    console.log('Slotted nodes changed:', nodes);
 7
 8    // Check if slot has content
 9    if (nodes.length === 0) {
10      console.log('Slot is empty');
11    }
12  });
13}

Default Slot Content

 1// Provide fallback content when slot is empty
 2render() {
 3  this.shadowRoot.innerHTML = `
 4    <slot name="icon">
 5      <!-- Fallback icon if none provided -->
 6      <svg>...</svg>
 7    </slot>
 8    <slot>
 9      <!-- Default content if nothing slotted -->
10      <p>No content provided</p>
11    </slot>
12  `;
13}

Event Handling

Internal Event Listeners

 1setupEventListeners() {
 2  const button = this.shadowRoot.querySelector('button');
 3  const overlay = this.shadowRoot.querySelector('.overlay');
 4
 5  // Event listeners on shadow DOM elements
 6  button.addEventListener('click', () => {
 7    this.toggle();
 8  });
 9
10  overlay.addEventListener('click', () => {
11    this.close();
12  });
13}
14
15// Always clean up in disconnectedCallback
16disconnectedCallback() {
17  // Remove listeners or use AbortController
18}

Using AbortController for Cleanup

 1connectedCallback() {
 2  this.controller = new AbortController();
 3  const { signal } = this.controller;
 4
 5  this.addEventListener('click', this.handleClick, { signal });
 6  window.addEventListener('resize', this.handleResize, { signal });
 7}
 8
 9disconnectedCallback() {
10  // Removes all listeners with one call
11  this.controller.abort();
12}

Event Bubbling Pattern

1// Stop propagation to prevent unwanted closures
2setupEventListeners() {
3  const sidebar = this.shadowRoot.querySelector('aside');
4
5  sidebar.addEventListener('click', (e) => {
6    e.stopPropagation(); // Prevent event from bubbling
7  });
8}

Custom Events (Component API)

 1open() {
 2  this.isOpen = true;
 3  // Update UI...
 4
 5  // Dispatch custom event with bubbling
 6  this.dispatchEvent(new CustomEvent('sidebar-opened', {
 7    bubbles: true,    // Event crosses shadow boundary
 8    composed: true,   // Event crosses multiple shadow roots
 9    detail: { timestamp: Date.now() }
10  }));
11}
12
13close() {
14  this.isOpen = false;
15  // Update UI...
16
17  this.dispatchEvent(new CustomEvent('sidebar-closed', {
18    bubbles: true,
19    composed: true
20  }));
21}
1// External usage
2const component = document.querySelector("my-component");
3
4component.addEventListener("sidebar-opened", (e) => {
5  console.log("Opened!", e.detail);
6});

Event Retargeting in Shadow DOM

 1// Events from shadow DOM are retargeted to the host element
 2// when they cross the shadow boundary
 3
 4// Inside component:
 5button.addEventListener("click", (e) => {
 6  console.log(e.target); // <button>
 7  console.log(e.composedPath()); // Full path including shadow elements
 8});
 9
10// Outside component:
11component.addEventListener("click", (e) => {
12  console.log(e.target); // <my-component> (retargeted)
13  console.log(e.composedPath()); // Access original path
14});

Public API Methods

Imperative API Pattern

 1class MyComponent extends HTMLElement {
 2  // Public methods for programmatic control
 3  open() {
 4    this.isOpen = true;
 5    const element = this.shadowRoot.querySelector("aside");
 6    element.classList.add("open");
 7    this.dispatchEvent(new CustomEvent("opened", { bubbles: true }));
 8  }
 9
10  close() {
11    this.isOpen = false;
12    const element = this.shadowRoot.querySelector("aside");
13    element.classList.remove("open");
14    this.dispatchEvent(new CustomEvent("closed", { bubbles: true }));
15  }
16
17  toggle() {
18    if (this.isOpen) {
19      this.close();
20    } else {
21      this.open();
22    }
23  }
24
25  // Expose data via properties
26  get value() {
27    return this._value;
28  }
29
30  set value(val) {
31    this._value = val;
32    this.render();
33  }
34}
1// Usage
2const sidebar = document.querySelector("my-sidebar");
3sidebar.open();
4sidebar.close();
5sidebar.toggle();
6console.log(sidebar.value);

State Management

Internal State Pattern

 1class ToggleComponent extends HTMLElement {
 2  constructor() {
 3    super();
 4    this.attachShadow({ mode: "open" });
 5
 6    // Initialize internal state (private)
 7    this._isOpen = false;
 8    this._isLoading = false;
 9  }
10
11  get isOpen() {
12    return this._isOpen;
13  }
14
15  set isOpen(value) {
16    this._isOpen = value;
17    this.updateUI();
18  }
19
20  toggle() {
21    // Update state first
22    this.isOpen = !this.isOpen;
23  }
24
25  updateUI() {
26    // Then update UI based on state
27    const element = this.shadowRoot.querySelector(".panel");
28    if (this.isOpen) {
29      element.classList.add("open");
30    } else {
31      element.classList.remove("open");
32    }
33  }
34}

Gotcha: Re-rendering Resets DOM

 1// ❌ PROBLEM: Event listeners lost on re-render
 2attributeChangedCallback(name, oldValue, newValue) {
 3  this.render(); // Replaces entire shadow DOM!
 4  this.setupEventListeners(); // Must re-attach listeners
 5}
 6
 7// ✅ BETTER: Update only what changed
 8attributeChangedCallback(name, oldValue, newValue) {
 9  if (name === 'width') {
10    const element = this.shadowRoot.querySelector('aside');
11    element.style.width = newValue;
12  }
13}
14
15// ✅ BEST: Use template elements (initialize once)
16constructor() {
17  super();
18  const template = document.getElementById('my-template');
19  this.attachShadow({ mode: 'open' })
20    .appendChild(template.content.cloneNode(true));
21  // Now you can update specific elements without re-rendering
22}

Customized Built-in Elements

Extending Native Elements

 1// Extend existing HTML elements
 2class FancyButton extends HTMLButtonElement {
 3  constructor() {
 4    super();
 5    this.addEventListener("click", () => {
 6      console.log("Fancy click!");
 7    });
 8  }
 9
10  connectedCallback() {
11    this.classList.add("fancy");
12  }
13}
14
15// Register with 'extends' option
16customElements.define("fancy-button", FancyButton, { extends: "button" });
1<!-- Use with 'is' attribute -->
2<button is="fancy-button">Click me</button>
3
4<!-- Or create programmatically -->
5<script>
6  const btn = document.createElement("button", { is: "fancy-button" });
7  document.body.appendChild(btn);
8</script>

Autonomous vs Customized Built-in

 1// Autonomous custom element (new tag)
 2class MyButton extends HTMLElement {
 3  // Creates <my-button>
 4}
 5customElements.define("my-button", MyButton);
 6
 7// Customized built-in element (extends existing)
 8class MyButton extends HTMLButtonElement {
 9  // Extends <button is="my-button">
10}
11customElements.define("my-button", MyButton, { extends: "button" });

Note: Safari does not support customized built-in elements. Use autonomous custom elements for better browser support.


Common Patterns

CSS Class Toggle Pattern

 1open() {
 2  this.isOpen = true;
 3  const sidebar = this.shadowRoot.querySelector('aside');
 4  const overlay = this.shadowRoot.querySelector('.overlay');
 5  const actionBar = this.shadowRoot.querySelector('.action-bar');
 6
 7  // Add state classes for CSS transitions
 8  sidebar.classList.add('open');
 9  overlay.classList.add('visible');
10  actionBar.classList.add('sidebar-open');
11
12  this.dispatchEvent(new CustomEvent('opened', { bubbles: true }));
13}

CSS Transitions with Classes

 1aside {
 2  transform: translateX(-100%);
 3  transition: transform 0.3s ease;
 4}
 5
 6aside.open {
 7  transform: translateX(0);
 8}
 9
10.overlay {
11  opacity: 0;
12  pointer-events: none;
13  transition: opacity 0.3s ease;
14}
15
16.overlay.visible {
17  opacity: 1;
18  pointer-events: auto;
19}

Conditional Template Rendering

 1render() {
 2  const isLeft = this.position === 'left';
 3  const showLabel = this.hasAttribute('show-label');
 4
 5  this.shadowRoot.innerHTML = `
 6    <style>
 7      :host {
 8        ${isLeft ? 'left: 0' : 'right: 0'};
 9      }
10    </style>
11
12    <div class="${this.active ? 'active' : ''}">
13      ${showLabel ? `<span>${this.label}</span>` : ''}
14    </div>
15  `;
16}

Loading External Stylesheets

 1connectedCallback() {
 2  const shadow = this.attachShadow({ mode: 'open' });
 3
 4  // Link external stylesheet
 5  const linkElem = document.createElement('link');
 6  linkElem.setAttribute('rel', 'stylesheet');
 7  linkElem.setAttribute('href', 'styles.css');
 8
 9  shadow.appendChild(linkElem);
10  // Add other content...
11}

Form Integration

 1class MyInput extends HTMLElement {
 2  // Make component form-associated
 3  static formAssociated = true;
 4
 5  constructor() {
 6    super();
 7    this.attachShadow({ mode: "open" });
 8    // Access the form
 9    this.internals = this.attachInternals();
10  }
11
12  connectedCallback() {
13    this.shadowRoot.innerHTML = `
14      <input type="text" id="input">
15    `;
16
17    const input = this.shadowRoot.querySelector("input");
18    input.addEventListener("input", (e) => {
19      // Set form value
20      this.internals.setFormValue(e.target.value);
21    });
22  }
23
24  // Form lifecycle
25  formResetCallback() {
26    this.shadowRoot.querySelector("input").value = "";
27  }
28
29  formDisabledCallback(disabled) {
30    this.shadowRoot.querySelector("input").disabled = disabled;
31  }
32}

Component Registration

Standard Registration

1customElements.define("my-component", MyComponent);

Gotcha: Naming Rules

 1// INVALID: Must have hyphen
 2customElements.define("mycomponent", MyComponent); // Throws error
 3
 4// INVALID: Cannot start with hyphen
 5customElements.define("-my-component", MyComponent); // Throws error
 6
 7// INVALID: Certain names are reserved
 8customElements.define("font-face", MyComponent); // Reserved
 9
10// VALID: Contains hyphen, lowercase
11customElements.define("my-component", MyComponent);
12customElements.define("x-button", XButton);

Check if Component Defined

 1// Wait for component to be defined
 2customElements.whenDefined("my-component").then(() => {
 3  console.log("Component ready!");
 4});
 5
 6// Check if already defined
 7if (customElements.get("my-component")) {
 8  console.log("Already registered");
 9}
10
11// Get the constructor
12const MyComponent = customElements.get("my-component");

Prevent Double Registration

1// Check before defining
2if (!customElements.get("my-component")) {
3  customElements.define("my-component", MyComponent);
4}

Best Practices

1. Always Use Shadow DOM for Encapsulation

1constructor() {
2  super();
3  this.attachShadow({ mode: 'open' }); // Prevents style leakage
4}

2. Provide Default Values for Attributes

1get width() {
2  return this.getAttribute('width') || '280px'; // Always have fallback
3}

3. Emit Events for State Changes

1toggle() {
2  this.isOpen = !this.isOpen;
3  // Let external code react to changes
4  this.dispatchEvent(new CustomEvent('toggled', {
5    bubbles: true,
6    composed: true,
7    detail: { isOpen: this.isOpen }
8  }));
9}

4. Use CSS Custom Properties for Theming

1:host {
2  --component-bg: var(--custom-bg, #ffffff); /* Allow override */
3  --component-color: var(--custom-color, #000000);
4}

5. Clean Up in disconnectedCallback

 1connectedCallback() {
 2  this.controller = new AbortController();
 3  window.addEventListener('resize', this.handleResize, {
 4    signal: this.controller.signal
 5  });
 6}
 7
 8disconnectedCallback() {
 9  this.controller.abort(); // Clean up all listeners
10}

6. Use Template Elements for Performance

1// Create template once, reuse many times
2const template = document.createElement('template');
3template.innerHTML = `...`;
4
5constructor() {
6  super();
7  this.attachShadow({ mode: 'open' })
8    .appendChild(template.content.cloneNode(true));
9}

7. Progressive Enhancement

1// Check if custom elements are supported
2if ("customElements" in window) {
3  customElements.define("my-component", MyComponent);
4} else {
5  // Provide fallback or load polyfill
6  console.warn("Custom elements not supported");
7}

Common Gotchas

1. Attribute Changes Before connectedCallback

1// Attributes may change before element is in DOM
2attributeChangedCallback(name, oldValue, newValue) {
3  if (!this.shadowRoot) return; // Not yet initialized
4  this.updateStyle();
5}

2. Constructor Timing

 1// WRONG: Too early - element not in DOM
 2constructor() {
 3  super();
 4  this.parentElement.style.color = 'red'; // parentElement is null!
 5}
 6
 7// CORRECT: Wait for connectedCallback
 8connectedCallback() {
 9  this.parentElement.style.color = 'red'; // Now it works
10}

3. querySelector Returns null for Shadow DOM

1// WRONG - Cannot query into shadow DOM from outside
2document.querySelector("my-component button"); // Returns null
3
4// CORRECT - Query from within component
5class MyComponent extends HTMLElement {
6  connectedCallback() {
7    const button = this.shadowRoot.querySelector("button"); // Works!
8  }
9}

4. Styles Don’t Inherit Into Shadow DOM

1/* Global styles */
2body {
3  font-family: Arial, sans-serif;
4  color: blue;
5}
 1// Shadow DOM styles are isolated - font and color won't inherit
 2render() {
 3  this.shadowRoot.innerHTML = `
 4    <style>
 5      /* Must redefine styles inside shadow DOM */
 6      :host {
 7        font-family: inherit; /* Inherit from parent */
 8        color: inherit;
 9      }
10    </style>
11  `;
12}

5. Re-rendering Loses State

 1// WRONG: Problem - input value lost on re-render
 2attributeChangedCallback() {
 3  this.render(); // innerHTML wipes out DOM, including user input
 4}
 5
 6// CORRECT: Solution - Preserve state or use selective updates
 7attributeChangedCallback(name, oldValue, newValue) {
 8  if (name === 'theme') {
 9    // Only update the specific style, don't re-render
10    this.shadowRoot.querySelector('.container').className = newValue;
11  }
12}

6. Timing of attachShadow

 1// Can only call attachShadow once
 2constructor() {
 3  super();
 4  this.attachShadow({ mode: 'open' });
 5  // this.attachShadow({ mode: 'open' }); // Error!
 6}
 7
 8// Both constructor and connectedCallback work
 9constructor() {
10  super();
11  this.attachShadow({ mode: 'open' }); // Common pattern
12}
13
14// OR
15
16connectedCallback() {
17  if (!this.shadowRoot) {
18    this.attachShadow({ mode: 'open' }); // Also valid
19  }
20}

7. Custom Elements Must Be Defined Before Use

 1// WRONG: Using before definition
 2<my-component></my-component>
 3<script>
 4  customElements.define('my-component', MyComponent);
 5</script>
 6
 7// CORRECT: Define before use, or use :not(:defined)
 8<script>
 9  customElements.define('my-component', MyComponent);
10</script>
11<my-component></my-component>
12
13// OR use CSS to hide until defined
14<style>
15  my-component:not(:defined) {
16    display: none;
17  }
18</style>

Performance Tips

1. Minimize Re-renders

 1// Bad: Full re-render on every change
 2attributeChangedCallback() {
 3  this.render();
 4}
 5
 6// Good: Surgical updates
 7attributeChangedCallback(name, oldValue, newValue) {
 8  const element = this.shadowRoot.querySelector('[data-attr="' + name + '"]');
 9  if (element) element.textContent = newValue;
10}

2. Debounce Expensive Operations

1attributeChangedCallback(name, oldValue, newValue) {
2  clearTimeout(this.renderTimeout);
3  this.renderTimeout = setTimeout(() => {
4    this.render();
5  }, 50);
6}

3. Use CSS for Animations, Not JavaScript

1/* Better performance */
2.panel {
3  transition: transform 0.3s ease;
4}
5
6.panel.open {
7  transform: translateX(0);
8}
1// Avoid requestAnimationFrame for simple transitions

4. Use Constructable Stylesheets

 1// Share styles across multiple instances
 2class MyComponent extends HTMLElement {
 3  static styles = new CSSStyleSheet();
 4
 5  static {
 6    this.styles.replaceSync(`
 7      :host { display: block; }
 8    `);
 9  }
10
11  constructor() {
12    super();
13    this.attachShadow({ mode: "open" });
14    // Reuse the same stylesheet (memory efficient)
15    this.shadowRoot.adoptedStyleSheets = [MyComponent.styles];
16  }
17}

5. Lazy Load Heavy Components

1// Define component only when needed
2async function loadHeavyComponent() {
3  const { HeavyComponent } = await import("./heavy-component.js");
4  customElements.define("heavy-component", HeavyComponent);
5}
6
7// Load on interaction
8button.addEventListener("click", loadHeavyComponent, { once: true });

Module Loading

ES6 Modules Pattern

1// component.js
2class MyComponent extends HTMLElement {
3  // ... implementation
4}
5
6customElements.define("my-component", MyComponent);
1<!-- Usage in HTML -->
2<script type="module" src="component.js"></script>

Multiple Components

1// Import multiple components
2import "./sidebar.js";
3import "./button.js";
4import "./panel.js";

Exporting for Reuse

 1// component.js
 2export class MyComponent extends HTMLElement {
 3  // ... implementation
 4}
 5
 6// Auto-register if desired
 7customElements.define("my-component", MyComponent);
 8
 9// main.js
10import { MyComponent } from "./component.js";
11// Can now extend or compose

Testing Patterns

Basic Component Test

 1// Create and test component
 2const component = document.createElement("my-component");
 3component.setAttribute("width", "300px");
 4document.body.appendChild(component);
 5
 6// Wait for component to be ready
 7await customElements.whenDefined("my-component");
 8
 9// Test shadow DOM
10const button = component.shadowRoot.querySelector("button");
11expect(button).toBeTruthy();
12
13// Test public API
14component.open();
15expect(component.isOpen).toBe(true);
16
17// Test events
18const handler = jest.fn();
19component.addEventListener("opened", handler);
20component.open();
21expect(handler).toHaveBeenCalled();
22
23// Clean up
24component.remove();

Testing Attributes

 1test("responds to attribute changes", async () => {
 2  const component = document.createElement("my-component");
 3  document.body.appendChild(component);
 4
 5  await customElements.whenDefined("my-component");
 6
 7  component.setAttribute("color", "blue");
 8
 9  // Wait for update
10  await new Promise((resolve) => setTimeout(resolve, 0));
11
12  const element = component.shadowRoot.querySelector(".box");
13  expect(element.style.backgroundColor).toBe("blue");
14});

Testing Slots

 1test("renders slotted content", async () => {
 2  const component = document.createElement("my-component");
 3  component.innerHTML = '<span slot="header">Title</span>';
 4  document.body.appendChild(component);
 5
 6  await customElements.whenDefined("my-component");
 7
 8  const slot = component.shadowRoot.querySelector('slot[name="header"]');
 9  const nodes = slot.assignedNodes();
10  expect(nodes[0].textContent).toBe("Title");
11});

Accessibility

ARIA Attributes

1connectedCallback() {
2  // Set ARIA attributes
3  if (!this.hasAttribute('role')) {
4    this.setAttribute('role', 'button');
5  }
6  if (!this.hasAttribute('tabindex')) {
7    this.setAttribute('tabindex', '0');
8  }
9}

Keyboard Navigation

1connectedCallback() {
2  this.addEventListener('keydown', (e) => {
3    if (e.key === 'Enter' || e.key === ' ') {
4      e.preventDefault();
5      this.click();
6    }
7  });
8}

Focus Management

 1class MyDialog extends HTMLElement {
 2  open() {
 3    this.isOpen = true;
 4    this.render();
 5
 6    // Trap focus in dialog
 7    const focusable = this.shadowRoot.querySelectorAll(
 8      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
 9    );
10    if (focusable.length > 0) {
11      focusable[0].focus();
12    }
13  }
14}

Quick Reference

PatternCode
Basic componentclass C extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); } }
RegistercustomElements.define('my-comp', MyComp)
Observe attrsstatic get observedAttributes() { return ['attr']; }
Get attrthis.getAttribute('name') || 'default'
Boolean attrthis.hasAttribute('active')
Dispatch eventthis.dispatchEvent(new CustomEvent('event', { bubbles: true, composed: true }))
Named slot<slot name="header"></slot>
Style slot::slotted(*)
CSS property--custom-prop: var(--override, default);
Query shadowthis.shadowRoot.querySelector('.class')
Template elementtemplate.content.cloneNode(true)
Lifecycle callbacksconnectedCallback(), disconnectedCallback(), adoptedCallback(), attributeChangedCallback()
Extend nativeclass C extends HTMLButtonElement { } customElements.define('c', C, { extends: 'button' })
Check definedcustomElements.whenDefined('my-comp')
AbortControllercontroller.abort() // cleanup

Browser Support

FeatureChromeFirefoxSafariEdge
Custom Elements54+63+10.1+79+
Shadow DOM53+63+10+79+
Template26+22+8+13+
Customized Built-in67+63+79+
Constructable Stylesheets73+101+16.4+79+

Note: For older browser support, use polyfills from webcomponents.org.


Tags: Web Components, JavaScript, Shadow DOM, Custom Elements, HTML Templates, Web Standards

Tags: Web-Components, Javascript, Shadow-Dom, Custom-Elements, Web-Standards, Frontend