JavaScript Core/
Lesson

Static pages are a thing of the past. Users expect applications to respond to every click, keystroke, and swipe. Event listeners are the bridge between your users and your code. They let you say: "When the user clicks this button, run this function." Without events, JavaScript would be blind to user actions.

Anatomy of an event listener

At its core, an event listener is simple: you tell the browser to watch for something, and what to do when it happens.

const button = document.querySelector('#myButton');

button.addEventListener('click', function(event) {
  console.log('Button was clicked!');
  console.log('Event type:', event.type);
});

The three pieces: the element you are watching, the event you are watching for, and the handler function that runs when it happens. The browser automatically passes an event object to your handler with details about what happened.

02

Common events

EventFires whenCommon use case
clickUser clicks elementButtons, links, cards
dblclickUser double-clicksSpecial actions
mouseenter / mouseleaveCursor enters/leavesHover effects, tooltips
keydown / keyupKey pressed/releasedShortcuts, game controls
inputInput value changesLive validation, search
changeInput loses focus with new valueForm validation
submitForm submittedHandle form data
focus / blurElement gains/loses focusValidation, styling
DOMContentLoadedDOM fully loadedSafe to query elements

Mouse events

card.addEventListener('click', (e) => {
  console.log('Clicked at:', e.clientX, e.clientY);
});

card.addEventListener('mouseenter', () => card.classList.add('hovered'));
card.addEventListener('mouseleave', () => card.classList.remove('hovered'));

Keyboard events

input.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    console.log('Enter pressed!');
  }

  if (e.ctrlKey && e.key === 's') {
    e.preventDefault();  // prevent browser save dialog
    saveDocument();
  }
});

Form events

form.addEventListener('submit', (e) => {
  e.preventDefault();  // stop page reload
  const data = new FormData(form);
  console.log('Email:', data.get('email'));
});

emailInput.addEventListener('input', (e) => {
  console.log('Current value:', e.target.value);
});
03

The event object

The event object is a goldmine of information:

document.addEventListener('click', (event) => {
  event.type;          // 'click'
  event.target;        // the element that was clicked
  event.currentTarget; // the element with the listener
  event.clientX;       // mouse X relative to viewport
  event.shiftKey;      // true if Shift was held
  event.preventDefault();    // stop default behavior
  event.stopPropagation();   // stop event bubbling
});

target vs currentTarget

<div id="container">
  <button id="btn">Click me</button>
</div>
container.addEventListener('click', (e) => {
  e.target;        // the button (what was actually clicked)
  e.currentTarget; // the container (what has the listener)
});
04

Event bubblingWhat is event bubbling?The default browser behavior where an event triggered on a child element propagates upward through each parent element, firing their handlers along the way. and delegation

When you click an element, the event fires on that element, then on its parent, then its grandparent, all the way up to document. This is called bubbling.

Event delegationWhat is event delegation?Attaching one event listener to a parent element instead of many listeners to each child, using event bubbling to catch events from children. uses bubbling to your advantage, one listener on a parent handles all child events:

// Bad: 100 buttons = 100 listeners
document.querySelectorAll('.delete-btn').forEach(btn => {
  btn.addEventListener('click', handleDelete);
});

// Good: 1 listener handles all buttons (including ones added later)
document.querySelector('#todo-list').addEventListener('click', (e) => {
  if (e.target.classList.contains('delete-btn')) {
    handleDelete(e.target);
  }
});

This is better because it uses less memory, works for elements added dynamically after the listener was attached, and is less code to maintain.

AI pitfall, listeners without cleanup, no delegation: AI adds event listeners everywhere but almost never removes them. Every addEventListener call should have a corresponding removeEventListener plan, especially in single-page apps where components mount and unmount. Without cleanup, listeners pile up every time a component re-renders, a classic memory leak that degrades performance over time. AI also ignores event delegation entirely. It will generate a forEach loop attaching individual listeners to 50 elements when one delegated listener on the parent would handle all of them, including elements added dynamically in the future. If you see AI adding listeners inside a loop, consider delegation instead. And if the AI-generated component has a React useEffect that adds a listener, always check that it returns a cleanup function, return () => document.removeEventListener(...).
05

Removing event listeners

Listeners stay attached until you remove them. This matters for memory management:

function handleClick() {
  console.log('Clicked');
}

button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);  // must be same function reference

Anonymous functions cannot be removed:

// CANNOT be removed later - avoid this pattern
button.addEventListener('click', () => console.log('Clicked'));

// CAN be removed - use named functions
function handler() { console.log('Clicked'); }
button.addEventListener('click', handler);
button.removeEventListener('click', handler);

One-time listeners

button.addEventListener('click', () => {
  console.log('This runs only once');
}, { once: true });

The { once: true } option is the cleanest way to handle one-shot events. The browser removes the listener automatically after the first call, no cleanup needed on your end.

06

Quick reference

TaskCode
Add listenerel.addEventListener('click', handler)
Remove listenerel.removeEventListener('click', handler)
Prevent defaultevent.preventDefault()
Stop bubblingevent.stopPropagation()
Get clicked elementevent.target
Get listener elementevent.currentTarget
One-time listenerel.addEventListener('click', fn, { once: true })
Wait for DOMdocument.addEventListener('DOMContentLoaded', fn)
Event delegationListen on parent, check e.target