javascript 264 lines · 1 tab

Web Components and Shadow DOM encapsulation

Alex Chang Feb 2026
1 tab
// 1. Basic Custom Element
class MyButton extends HTMLElement {
  constructor() {
    super();

    // Attach shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });

    // Create elements
    const button = document.createElement('button');
    button.textContent = this.getAttribute('text') || 'Click me';

    // Add styles
    const style = document.createElement('style');
    style.textContent = `
      button {
        padding: 10px 20px;
        background: #4285f4;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
      }
      button:hover {
        background: #357abd;
      }
    `;

    shadow.appendChild(style);
    shadow.appendChild(button);

    button.addEventListener('click', () => {
      this.dispatchEvent(new CustomEvent('myclick', {
        bubbles: true,
        composed: true,
        detail: { message: 'Button clicked!' }
      }));
    });
  }
}

// Register the custom element
customElements.define('my-button', MyButton);

// Usage: <my-button text="Submit"></my-button>

// 2. Component with Template and Slot
class UserCard extends HTMLElement {
  constructor() {
    super();

    const shadow = this.attachShadow({ mode: 'open' });

    // Create template
    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        :host {
          display: block;
          border: 1px solid #ddd;
          border-radius: 8px;
          padding: 1rem;
          max-width: 300px;
        }

        .avatar {
          width: 64px;
          height: 64px;
          border-radius: 50%;
          object-fit: cover;
        }

        .name {
          font-size: 1.25rem;
          font-weight: bold;
          margin: 0.5rem 0;
        }

        ::slotted(p) {
          color: #666;
        }
      </style>

      <div class="card">
        <img class="avatar" src="" alt="Avatar">
        <h3 class="name"></h3>
        <slot name="bio"></slot>
        <slot></slot>
      </div>
    `;

    shadow.appendChild(template.content.cloneNode(true));
  }

  // Lifecycle callbacks
  connectedCallback() {
    console.log('Component added to page');
    this.render();
  }

  disconnectedCallback() {
    console.log('Component removed from page');
  }

  adoptedCallback() {
    console.log('Component moved to new page');
  }

  // Observed attributes
  static get observedAttributes() {
    return ['name', 'avatar'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
    this.render();
  }

  render() {
    const name = this.getAttribute('name') || 'Anonymous';
    const avatar = this.getAttribute('avatar') || '/default-avatar.png';

    const nameEl = this.shadowRoot.querySelector('.name');
    const avatarEl = this.shadowRoot.querySelector('.avatar');

    if (nameEl) nameEl.textContent = name;
    if (avatarEl) avatarEl.src = avatar;
  }
}

customElements.define('user-card', UserCard);

/* Usage:
<user-card name="Alex Chen" avatar="/alex.jpg">
  <p slot="bio">Front-end developer</p>
  <button>Follow</button>
</user-card>
*/

// 3. Advanced component with properties and methods
class TodoList extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this._todos = [];
  }

  connectedCallback() {
    this.render();
    this.attachEventListeners();
  }

  // Public API
  addTodo(text) {
    this._todos.push({ id: Date.now(), text, completed: false });
    this.render();
  }

  removeTodo(id) {
    this._todos = this._todos.filter(todo => todo.id !== id);
    this.render();
  }

  toggleTodo(id) {
    const todo = this._todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
      this.render();
    }
  }

  // Getters/setters
  get todos() {
    return this._todos;
  }

  set todos(value) {
    this._todos = value;
    this.render();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        ul { list-style: none; padding: 0; }
        li {
          padding: 0.5rem;
          border-bottom: 1px solid #eee;
          display: flex;
          align-items: center;
          gap: 0.5rem;
        }
        .completed { text-decoration: line-through; opacity: 0.6; }
        button { padding: 0.25rem 0.5rem; }
      </style>

      <div>
        <input type="text" id="new-todo" placeholder="Add todo">
        <button id="add-btn">Add</button>
      </div>

      <ul>
        ${this._todos.map(todo => `
          <li>
            <input
              type="checkbox"
              ${todo.completed ? 'checked' : ''}
              data-id="${todo.id}"
            >
            <span class="${todo.completed ? 'completed' : ''}">
              ${todo.text}
            </span>
            <button data-id="${todo.id}" class="delete-btn">Delete</button>
          </li>
        `).join('')}
      </ul>
    `;

    this.attachEventListeners();
  }

  attachEventListeners() {
    const addBtn = this.shadowRoot.querySelector('#add-btn');
    const input = this.shadowRoot.querySelector('#new-todo');

    addBtn?.addEventListener('click', () => {
      if (input.value.trim()) {
        this.addTodo(input.value.trim());
        input.value = '';
      }
    });

    this.shadowRoot.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
      checkbox.addEventListener('change', (e) => {
        this.toggleTodo(Number(e.target.dataset.id));
      });
    });

    this.shadowRoot.querySelectorAll('.delete-btn').forEach(btn => {
      btn.addEventListener('click', (e) => {
        this.removeTodo(Number(e.target.dataset.id));
      });
    });
  }
}

customElements.define('todo-list', TodoList);

// 4. Extending built-in elements
class FancyButton extends HTMLButtonElement {
  connectedCallback() {
    this.classList.add('fancy');
    this.addEventListener('click', this.handleClick);
  }

  handleClick() {
    this.classList.add('clicked');
    setTimeout(() => this.classList.remove('clicked'), 300);
  }
}

customElements.define('fancy-button', FancyButton, { extends: 'button' });

// Usage: <button is="fancy-button">Click me</button>
1 file · javascript Explain with highlit

Web Components create reusable custom elements with encapsulated functionality. I use Custom Elements API to define new HTML tags with customElements.define(). Shadow DOM provides style and markup encapsulation preventing CSS leakage. HTML Templates with <template> and <slot> enable flexible component structures. The attachShadow() method creates shadow root for isolated DOM. Lifecycle callbacks like connectedCallback() and disconnectedCallback() manage component lifecycle. Attributes and properties enable component configuration. Web Components work with any framework or vanilla JavaScript. They provide true component reusability across projects and frameworks.


Related snips

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Semantic HTML Example</title>

Semantic HTML5 elements and accessibility best practices

html html5 semantics
by Alex Chang 2 tabs
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
javascript
import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"

export default class extends Controller {
  connect() {
    // Global shortcuts

Keyboard shortcuts with Stimulus and Mousetrap

stimulus javascript ux
by Jordan Lee 2 tabs
javascript
// Get canvas and context
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// 1. Basic shapes
// Rectangle (filled)

Canvas API for graphics and animations

canvas javascript graphics
by Alex Chang 1 tab
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab
erb
<div class="layout">
  <ul class="product-list">
    <% @products.each do |product| %>
      <li class="product-row">
        <%= link_to product.name,
              product_path(product),

Turbo Frames: “details” panel that lazy-loads on click

rails turbo hotwire
by codesnips 4 tabs

Share this code

Here's the card — post it anywhere.

Web Components and Shadow DOM encapsulation — share card
Link copied