cd ../blog
11 min read Web APIs

Web Components in 2025: Building Custom Elements

Master the art of building reusable, framework-agnostic components using Web Components and Custom Elements API.

Web Components Custom Elements Frontend
Web Components in 2025: Building Custom Elements

## The Web Components Revolution

Web Components offer true reusability without framework overhead. They're the foundation of modern web standards.

## Creating Your First Custom Element

Define custom elements using the Custom Elements API:

javascript
class MyButton extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
  }

  connectedCallback() {
    this.render()
    this.addEventListener('click', () => this.handleClick())
  }

  handleClick() {
    this.dispatchEvent(new CustomEvent('button-clicked', {
      detail: { message: 'Button was clicked' },
      bubbles: true
    }))
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        button {
          padding: 10px 20px;
          background: #007bff;
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }
        button:hover {
          background: #0056b3;
        }
      </style>
      <button><slot>Click me</slot></button>
    `
  }
}

customElements.define('my-button', MyButton)

## Shadow DOM for Encapsulation

The Shadow DOM provides style and structure encapsulation:

javascript
class MyCard extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          border: 1px solid #ddd;
          border-radius: 8px;
          padding: 16px;
        }

        .header {
          font-weight: bold;
          margin-bottom: 8px;
        }
      </style>
      <div class="header"><slot name="title"></slot></div>
      <div class="content"><slot></slot></div>
    `
  }
}

customElements.define('my-card', MyCard)

## Slots for Flexible Content Distribution

Slots allow flexible content composition:

html
<my-card>
  <span slot="title">My Title</span>
  <p>This is the main content</p>
  <button slot="actions">Action Button</button>
</my-card>

## Attributes and Properties

Properly implement reactive attributes:

javascript
class MyComponent extends HTMLElement {
  static observedAttributes = ['disabled', 'label']

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'disabled') {
      this.updateDisabledState(newValue !== null)
    }
    if (name === 'label') {
      this.updateLabel(newValue)
    }
  }

  updateDisabledState(isDisabled) {
    const button = this.shadowRoot.querySelector('button')
    if (button) {
      button.disabled = isDisabled
    }
  }

  updateLabel(newLabel) {
    const label = this.shadowRoot.querySelector('.label')
    if (label) {
      label.textContent = newLabel
    }
  }
}

## Real-World Example: Reusable Modal Component

javascript
class ModalDialog extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
  }

  connectedCallback() {
    this.render()
    this.setupEventListeners()
  }

  setupEventListeners() {
    const closeBtn = this.shadowRoot.querySelector('.close')
    const backdrop = this.shadowRoot.querySelector('.backdrop')

    closeBtn?.addEventListener('click', () => this.close())
    backdrop?.addEventListener('click', () => this.close())
  }

  open() {
    this.setAttribute('open', '')
  }

  close() {
    this.removeAttribute('open')
    this.dispatchEvent(new CustomEvent('modal-closed'))
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          --z-index: 1000;
        }

        :host([open]) .backdrop {
          display: flex;
        }

        .backdrop {
          display: none;
          position: fixed;
          top: 0;
          left: 0;
          right: 0;
          bottom: 0;
          background: rgba(0, 0, 0, 0.5);
          align-items: center;
          justify-content: center;
          z-index: var(--z-index);
        }

        .dialog {
          background: white;
          padding: 24px;
          border-radius: 8px;
          max-width: 500px;
          position: relative;
        }

        .close {
          position: absolute;
          top: 8px;
          right: 8px;
          background: none;
          border: none;
          font-size: 24px;
          cursor: pointer;
        }
      </style>

      <div class="backdrop">
        <div class="dialog">
          <button class="close">×</button>
          <slot></slot>
        </div>
      </div>
    `
  }
}

customElements.define('modal-dialog', ModalDialog)

## Framework Integration

Web Components work seamlessly with any framework:

jsx
// React
<MyButton onClick={handleClick}>Click me</MyButton>

// Vue
<my-button @button-clicked="handleClick">Click me</my-button>

// Angular
<my-button (button-clicked)="handleClick($event)">Click me</my-button>

## Best Practices

  • 1.**Always extend HTMLElement** - Never create custom elements from scratch
  • 2.**Use Shadow DOM** - For proper encapsulation
  • 3.**Implement observedAttributes** - For reactive properties
  • 4.**Emit custom events** - For component communication
  • 5.**Document your API** - Make slots and attributes clear
  • ## Conclusion

    Web Components are the future of reusable web elements. Master them to build truly portable, framework-agnostic components.

    // Thanks for reading!