← Back to Home

Example 12: Modal Component with Slots

Learn about slots, composition, and accessibility

💡 Key Concepts: Slots & Composition

This example demonstrates advanced Web Component patterns:

Live Demo

Click the buttons below to open different modal configurations:

Welcome!

This is a simple modal using the default slot for content.

Try pressing ESC to close, or click outside the modal.

Contact Form

⚠️ Confirm Action

Are you sure you want to proceed with this action?

This action cannot be undone.

JavaScript Code

class ModalDialog extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' });
    
    // Create modal structure with slots
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: none;
          position: fixed;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          z-index: 1000;
        }
        
        :host([open]) {
          display: flex;
          align-items: center;
          justify-content: center;
        }
        
        .overlay {
          position: fixed;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          background: rgba(0, 0, 0, 0.5);
          animation: fadeIn 0.2s ease-out;
        }
        
        .modal {
          position: relative;
          background: white;
          border-radius: 0;
          padding: 0;
          max-width: 600px;
          width: 90%;
          max-height: 90vh;
          overflow: auto;
          box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15);
          animation: slideIn 0.3s ease-out;
        }
        
        .modal-header {
          padding: 24px;
          border-bottom: 1px solid #e8e3db;
        }
        
        .modal-body {
          padding: 24px;
        }
        
        .modal-footer {
          padding: 20px 24px;
          border-top: 1px solid #e8e3db;
          display: flex;
          justify-content: flex-end;
          gap: 12px;
        }
        
        .close-btn {
          position: absolute;
          top: 20px;
          right: 20px;
          background: none;
          border: none;
          font-size: 28px;
          cursor: pointer;
          color: #6b6b6b;
          line-height: 1;
          padding: 0;
          width: 32px;
          height: 32px;
        }
        
        .close-btn:hover {
          color: #1a1a1a;
        }
        
        @keyframes fadeIn {
          from { opacity: 0; }
          to { opacity: 1; }
        }
        
        @keyframes slideIn {
          from {
            opacity: 0;
            transform: translateY(-20px);
          }
          to {
            opacity: 1;
            transform: translateY(0);
          }
        }
      </style>
      
      <div class="overlay" part="overlay"></div>
      <div class="modal" role="dialog" aria-modal="true" part="modal">
        <button class="close-btn" aria-label="Close modal">×</button>
        <div class="modal-header">
          <slot name="header"><h2>Modal</h2></slot>
        </div>
        <div class="modal-body">
          <slot></slot>
        </div>
        <div class="modal-footer">
          <slot name="footer"></slot>
        </div>
      </div>
    `;
    
    this.setupEventListeners();
  }
  
  setupEventListeners() {
    // Close button
    const closeBtn = this.shadowRoot.querySelector('.close-btn');
    closeBtn.addEventListener('click', () => this.close());
    
    // Click overlay to close
    const overlay = this.shadowRoot.querySelector('.overlay');
    overlay.addEventListener('click', () => this.close());
    
    // Prevent modal content clicks from closing
    const modal = this.shadowRoot.querySelector('.modal');
    modal.addEventListener('click', (e) => e.stopPropagation());
    
    // ESC key to close
    this.handleKeyDown = (e) => {
      if (e.key === 'Escape' && this.hasAttribute('open')) {
        this.close();
      }
    };
  }
  
  open() {
    this.setAttribute('open', '');
    document.addEventListener('keydown', this.handleKeyDown);
    
    // Focus management
    this.previousFocus = document.activeElement;
    const modal = this.shadowRoot.querySelector('.modal');
    modal.focus();
    
    // Dispatch custom event
    this.dispatchEvent(new CustomEvent('modalopen', {
      bubbles: true,
      composed: true
    }));
  }
  
  close() {
    this.removeAttribute('open');
    document.removeEventListener('keydown', this.handleKeyDown);
    
    // Restore focus
    if (this.previousFocus) {
      this.previousFocus.focus();
    }
    
    // Dispatch custom event
    this.dispatchEvent(new CustomEvent('modalclose', {
      bubbles: true,
      composed: true
    }));
  }
  
  disconnectedCallback() {
    // Cleanup when element is removed
    document.removeEventListener('keydown', this.handleKeyDown);
  }
}

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

HTML Usage with Slots

<!-- Simple Modal -->
<modal-dialog id="myModal">
  <h2 slot="header">Modal Title</h2>
  <p>This goes in the default slot (body).</p>
  <div slot="footer">
    <button>OK</button>
  </div>
</modal-dialog>

<!-- Open the modal -->
<script>
  document.querySelector('#myModal').open();
</script>

Key Concepts Explained

1. Slots

Slots are placeholders that allow users to insert their own content into a component. There are two types:

2. Accessibility Features

3. Component Composition

Slots enable composition - you can nest components and custom content:

4. Lifecycle Management

Try It Yourself

Open the browser console and try:

// Get a modal
const modal = document.querySelector('#simpleModal');

// Open it
modal.open();

// Listen to events
modal.addEventListener('modalopen', () => console.log('Modal opened!'));
modal.addEventListener('modalclose', () => console.log('Modal closed!'));

Documentation

Next Step