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
| Pattern | Code |
|---|---|
| Basic component | class C extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); } } |
| Register | customElements.define('my-comp', MyComp) |
| Observe attrs | static get observedAttributes() { return ['attr']; } |
| Get attr | this.getAttribute('name') || 'default' |
| Boolean attr | this.hasAttribute('active') |
| Dispatch event | this.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 shadow | this.shadowRoot.querySelector('.class') |
| Template element | template.content.cloneNode(true) |
| Lifecycle callbacks | connectedCallback(), disconnectedCallback(), adoptedCallback(), attributeChangedCallback() |
| Extend native | class C extends HTMLButtonElement { } customElements.define('c', C, { extends: 'button' }) |
| Check defined | customElements.whenDefined('my-comp') |
| AbortController | controller.abort() // cleanup |
Browser Support
| Feature | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| Custom Elements | 54+ | 63+ | 10.1+ | 79+ |
| Shadow DOM | 53+ | 63+ | 10+ | 79+ |
| Template | 26+ | 22+ | 8+ | 13+ |
| Customized Built-in | 67+ | 63+ | ❌ | 79+ |
| Constructable Stylesheets | 73+ | 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