Courses / JavaScript / Reusable UI Components
    Back to Course

    💡 Running Code Locally

    Custom Elements and Shadow DOM work in all modern browsers. To test these components:

    • Create an HTML file and add the JavaScript code in a <script> tag
    • Open the file in Chrome, Firefox, Safari, or Edge
    • Use your browser's DevTools (F12) to inspect Shadow DOM elements
    • Visit caniuse.com for browser support details

    Building Reusable UI Components with Pure JavaScript

    Master the foundations of component architecture without frameworks. Learn Custom Elements, Shadow DOM, slots, state management, and patterns used by React, Vue, and Svelte.

    What You'll Learn

    • Factory function components
    • Shadow DOM encapsulation
    • Custom Elements lifecycle
    • Slots for content injection
    • Global state management
    • Event bus communication

    What Are Reusable UI Components?

    A reusable component is a self-contained piece of UI with its own markup, styles, logic, and state. It can be instantiated multiple times, accepts inputs (props), emits events, and manages its own lifecycle.

    Real-World Component Examples

    • Dropdown menus• Modal dialogs• Tooltips• Tabs• Accordions• Notification toasts• Carousels• Form inputs

    💡 Why Pure JS? Zero dependencies, faster than framework components, works anywhere, teaches real DOM mastery, and you can port logic to React/Vue easily.

    Factory Function Pattern

    The simplest pattern for creating reusable components. A function returns an object with the DOM element and a public API for external control.

    Factory Function Pattern - Counter

    Create reusable components using factory functions

    Try it Yourself »
    JavaScript
    // Factory Function Pattern - Simple Counter Component
    function createCounter({ start = 0 }) {
      // Internal state
      let count = start;
    
      // Create DOM structure
      const root = document.createElement("div");
      root.className = "counter";
    
      root.innerHTML = `
        <button class="dec">-</button>
        <span class="value">${count}</span>
        <button class="inc">+</button>
      `;
    
      const valueEl = root.querySelector(".value");
    
      // Update function - re-renders when state changes
      function update() {
    ...

    Component Anatomy

    • State — Internal variables (count, open, selected)
    • DOM — The element structure created by the component
    • Update — Function that syncs DOM with state
    • Events — Internal event listeners
    • Public API — Methods exposed for external control

    Template Clone Pattern

    For HTML-driven components, use the <template> element. Templates are not rendered but can be cloned efficiently, making this pattern very fast.

    Template Clone Pattern

    Use template elements for HTML-driven components

    Try it Yourself »
    JavaScript
    // Template Clone Pattern - HTML-driven components
    // HTML template (would be in your HTML file):
    // <template id="card-template">
    //   <div class="card">
    //     <h3 class="title"></h3>
    //     <p class="content"></p>
    //   </div>
    // </template>
    
    function createCard({ title, content, onClick }) {
      // Clone the template
      const template = document.querySelector("#card-template");
      const root = template.content.cloneNode(true);
    
      // Fill in data
      root.querySelector(".title").textContent = title;
    ...

    Component Communication with Custom Events

    Components emit custom events to communicate with parent code. This keeps components loosely coupled and reusable across different contexts.

    Custom Events Communication

    Components emit custom events to communicate with parents

    Try it Yourself »
    JavaScript
    // Component Communication with Custom Events
    function createNotificationBanner() {
      const root = document.createElement("div");
      root.className = "notification-banner";
    
      root.innerHTML = `
        <span class="message"></span>
        <button class="close">×</button>
      `;
    
      const messageEl = root.querySelector(".message");
      const closeBtn = root.querySelector(".close");
    
      // Emit custom event when closed
      closeBtn.addEventListener("click", () => {
        // Dispatch a custom event that parents can
    ...

    Shadow DOM — Full Encapsulation

    Shadow DOM creates a private DOM tree inside your element. Styles are completely isolated — no leakage to or from global CSS. This is how browsers implement <video>, <input>, and <select>.

    Shadow DOM Encapsulation

    Create private DOM trees with isolated styles

    Try it Yourself »
    JavaScript
    // Shadow DOM - Full UI Encapsulation
    class MyCard extends HTMLElement {
      constructor() {
        super();
    
        // Attach shadow DOM (private DOM tree)
        this.attachShadow({ mode: "open" });
    
        this.shadowRoot.innerHTML = `
          <style>
            /* These styles are COMPLETELY isolated */
            .card {
              padding: 16px;
              background: white;
              border-radius: 8px;
              box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            }
            .card h3 {
              margin: 0 0 8px 0;
       
    ...

    ✅ Shadow DOM Benefits: Style encapsulation, no naming conflicts, framework-level component scoping, and markup isolation.

    Custom Elements with Lifecycle

    Custom Elements are the browser's native component system. They have lifecycle callbacks similar to React, can observe attribute changes, and are fully reusable.

    Custom Elements with Lifecycle

    Browser-native components with lifecycle callbacks

    Try it Yourself »
    JavaScript
    // Full Custom Element with Lifecycle
    class ToggleSwitch extends HTMLElement {
      // Declare which attributes to watch
      static get observedAttributes() {
        return ["checked", "disabled"];
      }
    
      constructor() {
        super();
        this.attachShadow({ mode: "open" });
    
        this.shadowRoot.innerHTML = `
          <style>
            :host {
              display: inline-block;
            }
            :host([disabled]) {
              opacity: 0.5;
              pointer-events: none;
            }
            .switch {
              wid
    ...

    Lifecycle Callbacks

    • constructor() — Called when element is created
    • connectedCallback() — Called when added to DOM
    • disconnectedCallback() — Called when removed from DOM
    • attributeChangedCallback() — Called when observed attribute changes

    Named Slots for Flexible Composition

    Slots let users inject content into specific areas of your component — similar to React's children prop but with named targets.

    Named Slots for Composition

    Inject content into specific areas of components

    Try it Yourself »
    JavaScript
    // Named Slots for Flexible Component Composition
    class ModalDialog extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: "open" });
    
        this.shadowRoot.innerHTML = `
          <style>
            :host {
              display: none;
            }
            :host([open]) {
              display: block;
            }
            .overlay {
              position: fixed;
              inset: 0;
              background: rgba(0,0,0,0.5);
              display: flex;
              align-items: center;
              justify
    ...

    Component Factory Pattern

    Generate multiple components from a single factory function. This pattern lets you create consistent UI libraries with minimal code duplication.

    Component Factory Pattern

    Generate multiple components from a single factory

    Try it Yourself »
    JavaScript
    // Component Factory - Generate Components from Config
    function createComponent({ name, template, style, methods = {} }) {
      class GeneratedComponent extends HTMLElement {
        constructor() {
          super();
          this.attachShadow({ mode: "open" });
          
          this.shadowRoot.innerHTML = `
            <style>${style}</style>
            ${template}
          `;
    
          // Call init if provided
          if (methods.init) {
            methods.init.call(this);
          }
        }
    
        connectedCallback() {
          methods.
    ...

    Global State Management

    For shared state across components (theme, user session, notifications), implement a simple store with subscribers — the same pattern used by Redux and Vuex.

    Global State Management

    Implement a simple store with subscribers like Redux

    Try it Yourself »
    JavaScript
    // Global State Store (Redux/Vuex Pattern)
    const Store = {
      state: {
        theme: "light",
        user: null,
        notifications: []
      },
      listeners: new Set(),
    
      // Get current state
      getState() {
        return { ...this.state };
      },
    
      // Update state
      set(key, value) {
        this.state[key] = value;
        this.notify();
      },
    
      // Subscribe to changes
      subscribe(callback) {
        this.listeners.add(callback);
        // Return unsubscribe function
        return () => this.listeners.delete(callback);
      },
    
     
    ...

    Event Bus for Cross-Component Communication

    An event bus allows any component to emit or listen to events without direct references. This enables loose coupling between unrelated components.

    Event Bus Pattern

    Enable loose coupling between unrelated components

    Try it Yourself »
    JavaScript
    // Event Bus for Cross-Component Communication
    const EventBus = {
      events: new Map(),
    
      // Subscribe to an event
      on(event, callback) {
        if (!this.events.has(event)) {
          this.events.set(event, new Set());
        }
        this.events.get(event).add(callback);
    
        // Return unsubscribe function
        return () => this.events.get(event).delete(callback);
      },
    
      // Emit an event
      emit(event, data) {
        const callbacks = this.events.get(event);
        if (callbacks) {
          callbacks.forEach(fn =>
    ...

    Async Components

    Components that load data before rendering are essential for real applications. Handle loading states, errors, and dynamic attribute changes.

    Async Components

    Components that load data before rendering

    Try it Yourself »
    JavaScript
    // Async Components - Load Data Before Rendering
    class UserProfile extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: "open" });
    
        this.shadowRoot.innerHTML = `
          <style>
            :host { display: block; }
            .loading {
              padding: 20px;
              text-align: center;
              color: #6b7280;
            }
            .error {
              padding: 20px;
              color: #ef4444;
              background: #fef2f2;
              border-radius: 8px;
            }
           
    ...

    Accessibility Patterns

    Professional components must be accessible. Implement ARIA roles, keyboard navigation, focus management, and screen reader support.

    Accessibility Patterns

    ARIA roles, keyboard navigation, and screen reader support

    Try it Yourself »
    JavaScript
    // Accessible Custom Components
    class AccessibleTabs extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: "open" });
        this.selectedIndex = 0;
    
        this.shadowRoot.innerHTML = `
          <style>
            .tablist {
              display: flex;
              border-bottom: 2px solid #e5e7eb;
            }
            .tab {
              padding: 12px 20px;
              background: none;
              border: none;
              cursor: pointer;
              font-size: 14px;
              color: #6b7280;
     
    ...

    💡 Accessibility Checklist: ARIA roles, keyboard navigation (Arrow keys, Home, End), focus states, aria-selected/aria-hidden, proper tabindex, and screen reader labels.

    High-Performance Patterns

    For large-scale applications, optimize with template caching, batched updates, object pooling, lazy initialization, and event delegation.

    High-Performance Patterns

    Template caching, batched updates, and object pooling

    Try it Yourself »
    JavaScript
    // High-Performance Component Patterns
    
    // 1. Template Caching - Clone instead of recreate
    class OptimizedList extends HTMLElement {
      static template = null;
    
      constructor() {
        super();
        this.attachShadow({ mode: "open" });
    
        // Cache template once
        if (!OptimizedList.template) {
          const tmpl = document.createElement("template");
          tmpl.innerHTML = `
            <style>
              .item { padding: 12px; border-bottom: 1px solid #eee; }
            </style>
            <div class="item">
    ...

    Key Takeaways

    Component Patterns

    • ✓ Factory functions for simple components
    • ✓ Custom Elements for browser-native components
    • ✓ Shadow DOM for style encapsulation
    • ✓ Slots for flexible content injection
    • ✓ Template cloning for fast rendering

    Communication & State

    • ✓ Custom events for parent communication
    • ✓ Event bus for cross-component messaging
    • ✓ Global store for shared state
    • ✓ One-way data flow (data down, events up)
    • ✓ Attribute observers for reactivity

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous