Skip to the content.
Agent Skill Available Download this Agent Skill (SKILL.md) to drop into ~/.claude/skills/ or any Agent-Skills-compatible runtime for AI-assisted Events work.
Download
events

Events

Key Insight

Events are the browser’s way of saying “something happened” — not just clicks, but scrolls, hovers, form submissions, page loads, network responses, animations completing, and hundreds of other moments your code can react to. The event system transforms the browser from a static document viewer into an interactive application platform. Understanding event propagation (bubbling/capturing), delegation, and preventDefault is what separates developers who can “make buttons work” from those who can build sophisticated, performant UIs with proper keyboard navigation, form validation, and infinite scroll.

Detailed Description

Events are fired to notify code of “interesting changes” that may affect code execution. These can arise from user interactions such as using a mouse or resizing a window, changes in the state of the underlying environment (e.g. low battery or media events from the operating system), and other causes.

The browser’s event system is based on the observer pattern: you register listeners for specific event types, and when those events occur, your callback functions execute. Events follow a propagation path through the DOM tree, enabling powerful patterns like event delegation. The Event object passed to handlers contains rich information about what happened (which key, mouse position, target element, etc.).

Key characteristics of events:

  1. Event types - Click, keydown, submit, load, scroll, etc. (100+ types)
  2. Event propagation - Events flow through DOM (capturing → target → bubbling)
  3. Event object - Contains information about the event (target, type, timestamp)
  4. Default behaviors - Many events have browser default actions (links navigate, forms submit)
  5. Custom events - You can create and dispatch your own events

Code Examples

Basic Example: Click + form events across frameworks

A click event is one idea with four spellings. Here is the same “dispatch an onClick when the user presses the Button atom” wiring taken straight from each chota-* template.

React

// templates/chota-react-redux/src/ui/atoms/Button/Button.component.jsx
// React passes handlers as props. camelCase `onClick` is the React-specific
// synthetic event; native DOM behaviour still bubbles underneath.
export default function Button(props) {
  const transformedProps = { ...props };
  delete transformedProps.isLoading;
  if (props.isLoading) { /* loading state */ }
  return <button {...transformedProps}>{props.children}</button>;
}

// Usage:
<Button onClick={(e) => handleClick(e)}>Save</Button>

Angular

// templates/chota-angular-ngrx/src/ui/atoms/Button/Button.component.ts
// @Output + EventEmitter is Angular's way of exposing "custom events"
// to the parent. The template binds (click) to call emit.
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({ selector: 'app-button', /* ... */ })
export default class ButtonComponent {
  @Input() isLoading = false;
  @Output() onClick = new EventEmitter<Event>();
}
<!-- Button.component.html -->
<button [type]="type || 'button'"
        (click)="onClick.emit($event)"
        [class]="computedClasses">
  <ng-content></ng-content>
</button>

<!-- Parent usage -->
<app-button (onClick)="handleClick($event)">Save</app-button>

Vue

<!-- templates/chota-vue-pinia/src/ui/atoms/Button/Button.component.vue -->
<template>
  <button :disabled="disabled" :type="type"
          @click="$emit('onClick')"
          :class="getButtonClass()">
    <slot />
  </button>
</template>

<!-- Parent usage -->
<!-- <Button @onClick="handleClick">Save</Button> -->

Web Components

// templates/chota-wc-saga/src/ui/atoms/Button/Button.component.js
// The WC templates use a tiny emit() helper that dispatches a CustomEvent.
// Parents listen with addEventListener or Lit's @onClick= directive.
import { html } from "lit";
import emit from "../../../utils/events/emit";

export default function Button(props) {
  return html`
    <button type="button"
      class="${props.classes}"
      @click="${() => emit(this, "onClick", props)}">
      <slot></slot>
    </button>
  `;
}

// utils/events/emit.js (simplified):
// export default function emit(host, name, detail) {
//   host.dispatchEvent(new CustomEvent(name, {
//     detail, bubbles: true, composed: true,
//   }));
// }

// Parent usage:
// <app-button @onClick=${(e) => handleClick(e.detail)}>Save</app-button>

The conceptual model is the same in every tab: the button’s native click listener calls something that re-broadcasts to the parent under a framework-specific conduit. React uses the synthetic event system + prop functions. Angular wraps RxJS around EventEmitter. Vue uses a named $emit. Web Components use the browser’s own CustomEvent bus. Parents subscribe in their respective idioms (onClick={...} / (onClick)="..." / @onClick="..." / @onClick=${...}) — but every one of those resolves to the same DOM click bubble under the hood.

Practical Example: Event Propagation and Delegation

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Event Propagation Example</title>
  <style>
    .outer { padding: 20px; background: lightblue; }
    .middle { padding: 20px; background: lightgreen; margin: 10px; }
    .inner { padding: 20px; background: lightcoral; margin: 10px; }
  </style>
</head>
<body>
  <div class="outer" id="outer">
    Outer
    <div class="middle" id="middle">
      Middle
      <div class="inner" id="inner">
        Inner (Click me!)
      </div>
    </div>
  </div>
  
  <script>
    const outer = document.getElementById('outer');
    const middle = document.getElementById('middle');
    const inner = document.getElementById('inner');
    
    // Event bubbling (default)
    outer.addEventListener('click', (e) => {
      console.log('Outer clicked (bubbling)');
    });
    
    middle.addEventListener('click', (e) => {
      console.log('Middle clicked (bubbling)');
      // Stop propagation to prevent outer from receiving event
      // e.stopPropagation();
    });
    
    inner.addEventListener('click', (e) => {
      console.log('Inner clicked (bubbling)');
      console.log('Event target:', e.target);
      console.log('Current target:', e.currentTarget);
    });
    
    // Event capturing (runs before bubbling)
    outer.addEventListener('click', (e) => {
      console.log('Outer clicked (capturing)');
    }, true);  // Third parameter = useCapture
    
    // Click on inner element outputs:
    // Outer clicked (capturing)  ← Capturing phase (top to bottom)
    // Inner clicked (bubbling)    ← Target phase
    // Middle clicked (bubbling)   ← Bubbling phase (bottom to top)
    // Outer clicked (bubbling)
  </script>
  
  <!-- Event Delegation Example -->
  <ul id="todoList">
    <li data-id="1">Task 1 <button class="delete">Delete</button></li>
    <li data-id="2">Task 2 <button class="delete">Delete</button></li>
    <li data-id="3">Task 3 <button class="delete">Delete</button></li>
  </ul>
  
  <script>
    // Event delegation - one listener for all buttons
    const todoList = document.getElementById('todoList');
    
    todoList.addEventListener('click', (event) => {
      // Check if delete button was clicked
      if (event.target.matches('button.delete')) {
        const listItem = event.target.closest('li');
        const taskId = listItem.dataset.id;
        
        console.log(`Deleting task ${taskId}`);
        listItem.remove();
      }
    });
    
    // Adding new items dynamically (no need to add listeners)
    const newTask = document.createElement('li');
    newTask.dataset.id = '4';
    newTask.innerHTML = 'Task 4 <button class="delete">Delete</button>';
    todoList.appendChild(newTask);
    // Delete button automatically works via delegation!
  </script>
</body>
</html>

Advanced Example: Custom Events and Event Patterns

// Custom events
class EventBus {
  constructor() {
    this.events = {};
  }
  
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }
  
  off(eventName, callback) {
    if (!this.events[eventName]) return;
    
    this.events[eventName] = this.events[eventName].filter(
      cb => cb !== callback
    );
  }
  
  emit(eventName, data) {
    if (!this.events[eventName]) return;
    
    this.events[eventName].forEach(callback => {
      callback(data);
    });
  }
}

// Usage
const eventBus = new EventBus();

eventBus.on('userLoggedIn', (user) => {
  console.log('User logged in:', user.name);
});

eventBus.on('userLoggedIn', (user) => {
  console.log('Welcome notification sent to', user.email);
});

eventBus.emit('userLoggedIn', { name: 'Alice', email: 'alice@example.com' });

// Native CustomEvent
const button = document.getElementById('customButton');

// Dispatch custom event
const customEvent = new CustomEvent('myCustomEvent', {
  detail: { message: 'Hello from custom event!', timestamp: Date.now() },
  bubbles: true,
  cancelable: true
});

button.dispatchEvent(customEvent);

// Listen for custom event
button.addEventListener('myCustomEvent', (event) => {
  console.log('Custom event received:', event.detail);
});

// Debouncing events
function debounce(func, delay) {
  let timeoutId;
  
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

// Usage: Debounce scroll event
const handleScroll = debounce(() => {
  console.log('Scrolled!', window.scrollY);
}, 300);

window.addEventListener('scroll', handleScroll);

// Throttling events
function throttle(func, limit) {
  let inThrottle;
  
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Usage: Throttle mousemove
const handleMouseMove = throttle((event) => {
  console.log('Mouse position:', event.clientX, event.clientY);
}, 100);

document.addEventListener('mousemove', handleMouseMove);

// Passive event listeners for scroll performance
document.addEventListener('scroll', handleScroll, {
  passive: true  // Tells browser we won't call preventDefault()
});

// Once option - listener automatically removed after first trigger
button.addEventListener('click', () => {
  console.log('This runs only once');
}, { once: true });

// Form validation with events
const form = document.getElementById('myForm');
const emailInput = document.getElementById('email');

emailInput.addEventListener('blur', (event) => {
  const email = event.target.value;
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  
  if (!emailRegex.test(email)) {
    event.target.setCustomValidity('Invalid email address');
    event.target.reportValidity();
  } else {
    event.target.setCustomValidity('');
  }
});

form.addEventListener('submit', (event) => {
  event.preventDefault();  // Prevent default form submission
  
  const formData = new FormData(event.target);
  const data = Object.fromEntries(formData);
  
  console.log('Form data:', data);
  
  // Send data via fetch
  fetch('/api/submit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  })
    .then(response => response.json())
    .then(result => console.log('Success:', result))
    .catch(error => console.error('Error:', error));
});

Common Mistakes

1. Not Removing Event Listeners (Memory Leaks)

Mistake: Adding listeners without cleanup causes memory leaks.

// ❌ BAD: Memory leak in SPA
function setupComponent() {
  const button = document.getElementById('button');
  
  button.addEventListener('click', () => {
    console.log('Clicked');
  });
  
  // Component unmounts but listener remains
}

// Called multiple times in SPA
setupComponent();  // Leak
setupComponent();  // Another leak
setupComponent();  // More leaks...
// ✅ GOOD: Clean up listeners
function setupComponent() {
  const button = document.getElementById('button');
  
  function handleClick() {
    console.log('Clicked');
  }
  
  button.addEventListener('click', handleClick);
  
  // Return cleanup function
  return () => {
    button.removeEventListener('click', handleClick);
  };
}

const cleanup = setupComponent();
// Later, when component unmounts
cleanup();

// Or use AbortController (modern)
const controller = new AbortController();

button.addEventListener('click', handleClick, {
  signal: controller.signal
});

// Clean up all listeners at once
controller.abort();

Why it matters: In SPAs, listeners accumulate without cleanup, causing memory leaks and duplicate event handling.

2. Using addEventListener Inside Loops Without Delegation

Mistake: Attaching individual listeners to many elements.

// ❌ BAD: 1000 event listeners
const items = document.querySelectorAll('.item');  // 1000 items

items.forEach(item => {
  item.addEventListener('click', handleClick);  // 1000 listeners!
});
// Memory intensive, slow performance
// ✅ GOOD: Event delegation (1 listener)
const container = document.getElementById('container');

container.addEventListener('click', (event) => {
  if (event.target.matches('.item')) {
    handleClick(event.target);
  }
});
// Single listener handles all items

Why it matters: 1000 listeners use 1000x more memory than delegation. Delegation also handles dynamically added elements automatically.

3. Forgetting preventDefault() on Form Submit

Mistake: Form submits causing page refresh.

// ❌ BAD: Page refreshes
const form = document.getElementById('myForm');

form.addEventListener('submit', (event) => {
  const data = new FormData(event.target);
  console.log('Data:', Object.fromEntries(data));
  // Page refreshes here! Data not sent via AJAX
});
// ✅ GOOD: Prevent default submit
form.addEventListener('submit', (event) => {
  event.preventDefault();  // Stop page refresh
  
  const data = new FormData(event.target);
  
  fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify(Object.fromEntries(data))
  });
});

Why it matters: Without preventDefault(), forms trigger browser’s default submit behavior (page refresh), breaking SPA user experience.

Types of HTML Element Events

Common Categories of HTML Element Events

Event Category Description Example Events
Mouse Events Triggered by mouse actions click, mouseover, mouseout, mousedown, mouseup, dblclick
Keyboard Events Triggered by keyboard actions keydown, keyup, keypress
Form Events Related to form input and submission submit, change, input, focus, blur, reset
Drag & Drop Events Involved in drag-and-drop operations drag, dragstart, dragend, dragenter, dragleave, dragover, drop
Clipboard Events Triggered by cut, copy, and paste actions copy, cut, paste
Media Events Related to media elements like audio and video play, pause, ended, volumechange, timeupdate
Focus Events Occur when elements gain or lose focus focus, blur, focusin, focusout
Load/Unload Events Related to loading/unloading of documents or resources load, unload, beforeunload
Touch Events Triggered by touch interactions (mobile devices) touchstart, touchend, touchmove, touchcancel
Wheel/Scroll Events Triggered by scrolling or mouse wheel actions scroll, wheel
Animation/Transition Related to CSS animations and transitions animationstart, animationend, transitionend
Input Events Triggered by changes to input fields input, change

Framework transformation of events

HTML events are handled differently in modern frameworks and libraries to provide enhanced developer experience, maintainability, and integration with their component models. Here’s how each major technology transforms and manages HTML element events:


React


Angular


Vue


Web Components


Comparison Table

Framework/Library Event Syntax Example Handler Binding Event Object Modifiers/Features
React <button onClick={fn}> Function in JSX SyntheticEvent Arrow functions for args, preventDefault, stopPropagation
Angular <button (click)="fn()"> Method in component $event (native) Key/code modifiers, template expressions
Vue <button @click="fn"> Method or inline Native event .stop, .prevent, .once, key modifiers
Web Components addEventListener('click', fn) Direct DOM binding Native event Custom events, event delegation, cleanup in lifecycle

Summary

HTML elements support a rich set of event types, including mouse, keyboard, form, media, drag-and-drop, clipboard, and more. Developers can attach event handlers using inline attributes, DOM properties, or the addEventListener method for robust and interactive web applications[1][2][3][5][6].

Each approach reflects the framework’s philosophy: React and Vue integrate events tightly with their component models, Angular provides declarative and expressive syntax with modifiers, and Web Components rely on native browser APIs for maximum flexibility and interoperability.

Quick Quiz

What is an HTML element event?

Which is the most flexible way to attach an event handler in plain HTML/JS?

What is the difference between event capturing and event bubbling?

What does React's SyntheticEvent give you over a raw DOM event?

When attaching a scroll or touchmove listener, why is `{ passive: true }` important?

References