Let's explore practical patterns that make CustomEvents powerful in Svelte 5 applications.
Pattern 1: Event Bus
Create a simple event bus for app-wide communication:
svelte<!-- eventBus.svelte.js --> <script module> const target = new EventTarget(); export function emit(eventName, detail) { target.dispatchEvent(new CustomEvent(eventName, { detail })); } export function on(eventName, handler) { target.addEventListener(eventName, handler); return () => target.removeEventListener(eventName, handler); } </script>
svelte<!-- Component A - emits --> <script> import { emit } from './eventBus.svelte.js'; </script> <button onclick={() => emit('cart:updated', { count: 5 })}> Add to Cart </button>
svelte<!-- Component B - listens --> <script> import { on } from './eventBus.svelte.js'; let cartCount = $state(0); $effect(() => { const unsubscribe = on('cart:updated', (e) => { cartCount = e.detail.count; }); return unsubscribe; // Cleanup on unmount }); </script> <span>Cart: {cartCount}</span>
Pattern 2: Action-Based Events
Create a Svelte action that dispatches custom events:
svelte<script> // Action that detects long press function longpress(node, duration = 500) { let timer; function handleMouseDown() { timer = setTimeout(() => { node.dispatchEvent(new CustomEvent('longpress', { bubbles: true, detail: { duration } })); }, duration); } function handleMouseUp() { clearTimeout(timer); } node.addEventListener('mousedown', handleMouseDown); node.addEventListener('mouseup', handleMouseUp); node.addEventListener('mouseleave', handleMouseUp); return { destroy() { node.removeEventListener('mousedown', handleMouseDown); node.removeEventListener('mouseup', handleMouseUp); node.removeEventListener('mouseleave', handleMouseUp); } }; } </script> <button use:longpress={1000} onlongpress={() => alert('Long pressed!')}> Hold me </button>
Pattern 3: Portal Communication
When components render in portals (like modals), CustomEvents bridge the gap:
svelte<!-- Modal content renders in a portal, outside normal tree --> <script> function handleConfirm() { // Can't use callback props - we're in a portal! // CustomEvent bubbles through the document document.dispatchEvent(new CustomEvent('modal:confirm', { detail: { modalId: 'delete-user', confirmed: true } })); } </script>
Pattern 4: Combining with Effects
svelte<script> let element; // Listen for custom events on a specific element $effect(() => { if (!element) return; function handler(event) { console.log('Custom event received:', event.detail); } element.addEventListener('mycustomevent', handler); return () => element.removeEventListener('mycustomevent', handler); }); </script> <div bind:this={element}> <ChildComponent /> </div>