Skip to content

Custom Events — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Custom Events. We cover key concepts, practical examples, and best practices to help you master this topic.

Custom events let you create and dispatch your own DOM events using CustomEvent, enabling decoupled component communication with detail data payloads.

What You'll Learn

  • How to create custom events with CustomEvent constructor
  • How to dispatch events with dispatchEvent
  • How to listen for custom events with addEventListener
  • How to pass data through the detail property
  • When to use custom events for component communication

Why It Matters

As applications grow, components need to communicate without tight coupling. Custom events provide a native DOM mechanism for this communication, reducing dependencies between components and making code more maintainable.

Real-World Use

  • A tab component dispatches a "tabchange" event when the active tab switches
  • A date picker dispatches a "dateselect" event with selected date range
  • A drag-and-drop library dispatches "dragstart", "dragover", and "drop" events
flowchart LR
  A[Component A] --> B[Create CustomEvent]
  B --> C[Add detail data]
  C --> D[dispatchEvent on element]
  D --> E[Event bubbles up]
  E --> F[Component B listener fires]
  F --> G[Read detail data]
  G --> H[Respond]

Creating and Dispatching Custom Events

Use the CustomEvent constructor to create events with custom data in the detail property.

const element = document.querySelector('.event-source');

// Create a basic custom event
const basicEvent = new CustomEvent('hello', {
    detail: { message: 'Hello from custom event!' }
});

element.dispatchEvent(basicEvent);

// Create with options
const userEvent = new CustomEvent('userLogin', {
    detail: {
        userId: 42,
        username: 'alice',
        role: 'admin',
        loginTime: Date.now()
    },
    bubbles: true,
    cancelable: true
});

element.dispatchEvent(userEvent);

// Listen for these events
element.addEventListener('hello', function(event) {
    console.log('Hello event:', event.detail.message);
});

element.addEventListener('userLogin', function(event) {
    console.log('User logged in:', event.detail.username);
    console.log('Role:', event.detail.role);
});

Expected output: The hello event logs its message. The userLogin event logs the username and role. The event.detail object carries all custom data.

Listening on Ancestors

Since custom events can bubble, ancestor elements can listen for them.

const button = document.querySelector('.action-btn');
const container = document.querySelector('.actions-container');
const main = document.querySelector('main');

// Dispatch from the button
button.addEventListener('click', function() {
    const event = new CustomEvent('actionPerformed', {
        detail: {
            action: 'save',
            timestamp: Date.now()
        },
        bubbles: true
    });
    this.dispatchEvent(event);
});

// Listen on the container
container.addEventListener('actionPerformed', function(event) {
    console.log('Container heard:', event.detail.action);
    // Show a toast notification
    showToast('Action saved successfully');
});

// Listen on a higher ancestor
main.addEventListener('actionPerformed', function(event) {
    console.log('Main heard:', event.detail.action);
    // Update the activity log
    updateActivityLog(event.detail);
});

Expected output: Clicking the button dispatches the custom event. It bubbles up through the container to the main element. Both listeners fire in order: container first, then main.

Canceling Custom Events

Custom events with cancelable: true can be prevented using preventDefault().

const form = document.querySelector('.checkout-form');

form.addEventListener('beforeCheckout', function(event) {
    // Validate cart
    const cart = getCartContents();
    if (cart.length === 0) {
        event.preventDefault();
        showError('Cannot checkout with empty cart');
    }
    if (!isUserLoggedIn()) {
        event.preventDefault();
        showError('Please log in to checkout');
    }
});

function triggerCheckout() {
    const event = new CustomEvent('beforeCheckout', {
        detail: { cartTotal: calculateTotal() },
        cancelable: true,
        bubbles: true
    });

    const cancelled = !form.dispatchEvent(event);
    if (!cancelled) {
        console.log('Proceeding with checkout');
        processCheckout();
    } else {
        console.log('Checkout prevented by validation');
    }
}

Expected output: If validation passes, dispatchEvent returns true and checkout proceeds. If validation fails, preventDefault() is called, dispatchEvent returns false, and checkout is cancelled.

Named Event Patterns

Use consistent naming conventions for custom events to avoid conflicts.

const mediaPlayer = document.querySelector('.media-player');

// Namespace events with prefixes
const events = {
    play: 'media:play',
    pause: 'media:pause',
    seek: 'media:seek',
    volumeChange: 'media:volume',
    trackEnd: 'media:trackEnd'
};

// Dispatch namespaced events
function dispatchMediaEvent(eventName, data) {
    const event = new CustomEvent(eventName, {
        detail: data,
        bubbles: true
    });
    mediaPlayer.dispatchEvent(event);
}

// Usage
mediaPlayer.addEventListener('media:play', function(event) {
    console.log('Playback started:', event.detail.track);
    updatePlayButton(true);
    startAnalytics('play', event.detail);
});

mediaPlayer.addEventListener('media:volume', function(event) {
    console.log('Volume changed to:', event.detail.level);
    updateVolumeDisplay(event.detail.level);
});

// dispatchMediaEvent('media:play', { track: 'Song A', position: 0 });

Expected output: The namespaced events do not conflict with native events. Each handler receives only the events it cares about. The colon-separated namespace is a common convention.

Event Bus Pattern

Use a shared element as an event bus for cross-component communication.

// Create a central event bus
const eventBus = document.createElement('div');
eventBus.style.display = 'none';
document.body.appendChild(eventBus);

// Component A: dispatches events
const userModule = {
    login(userData) {
        // Perform login...
        const event = new CustomEvent('auth:login', {
            detail: userData,
            bubbles: true
        });
        eventBus.dispatchEvent(event);
    },
    logout() {
        const event = new CustomEvent('auth:logout', {
            detail: {},
            bubbles: true
        });
        eventBus.dispatchEvent(event);
    }
};

// Component B: listens for events
const headerModule = {
    init() {
        eventBus.addEventListener('auth:login', function(event) {
            console.log('Header: user logged in, updating UI');
            document.querySelector('.user-name').textContent = event.detail.name;
            document.querySelector('.login-btn').classList.add('hidden');
            document.querySelector('.logout-btn').classList.remove('hidden');
        });

        eventBus.addEventListener('auth:logout', function() {
            console.log('Header: user logged out');
            document.querySelector('.user-name').textContent = '';
            document.querySelector('.login-btn').classList.remove('hidden');
            document.querySelector('.logout-btn').classList.add('hidden');
        });
    }
};

// Component C: analytics module
const analyticsModule = {
    init() {
        eventBus.addEventListener('auth:login', function(event) {
            console.log('Analytics: tracking login');
            trackEvent('login', { userId: event.detail.id });
        });
    }
};

headerModule.init();
analyticsModule.init();
// userModule.login({ id: 1, name: 'Alice' });

Expected output: When a user logs in, both the header and analytics modules respond to the same event without knowing about each other. This is decoupled communication.

Common Mistakes

  1. Not setting bubbles:true — If the custom event does not bubble, ancestor listeners never fire. Set bubbles: true unless you have a specific reason not to.
  2. Forgetting to listen for the event — A dispatched event with no listeners is silently ignored. There is no error. Ensure at least one listener exists if the event is important.
  3. Using generic event names — Names like "update" or "change" conflict with native events. Use namespaced names like "app:update" or "component:change".
  4. Expecting custom events to trigger browser defaults — Custom events do not trigger browser default behaviors. They are purely for application-level communication.
  5. Passing functions or DOM nodes in detail — The detail property can theoretically hold any value, but Serialization (for postMessage or storage) requires JSON-safe data. Keep detail simple.

Practice Questions

  1. How do you create a custom event with data? Use new CustomEvent('eventName', { detail: { key: 'value' } }). The detail property holds the data payload.
  2. What does dispatchEvent return? It returns a boolean. true means the event was not cancelled (preventDefault not called). false means the event was cancelled.
  3. Why should custom events use namespaced names? To avoid conflicts with native events and other custom events. A prefix like "app:" or "component:" prevents collisions.
  4. Challenge: Build a simple shopping cart where adding an item dispatches a "cart:add" event, removing dispatches "cart:remove", and the cart display and checkout button both listen for these events to update independently.

FAQ

Can custom events bubble like native events?

Yes. Set bubbles: true in the event options. The event travels up the DOM tree triggering ancestor listeners.

What is the difference between CustomEvent and Event constructor?

CustomEvent supports the detail property for passing data. Event does not. Use CustomEvent when you need to pass data with the event.

Can I dispatch the same custom event on multiple elements?

Yes. Create separate event instances (or reuse with different target) and dispatchEvent on each element.

Do custom events work with event delegation?

Yes. If bubbles is true, custom events can be delegated using the same patterns as native events.

Is there a performance penalty for using many custom events?

Custom events have similar performance to native events. Dispatching thousands per second may cause issues, but typical usage (tens per user action) is negligible.

Mini Project

Build a tab component using custom events. Each tab is a button. Clicking a tab dispatches a "tabs:select" custom event with the tab ID. The tab panel container listens for this event and shows/hides panels accordingly. A separate URL hash updater module also listens and updates window.location.hash. The tab component, panel container, and URL module are completely independent.

What's Next

Continue with Lesson 16: Form Events to learn about form-specific events like submit, change, input, and focus/blur for building interactive forms.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro