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.