Code of the Day
JavaScript in the BrowserJavaScript in the Browser

Event Listeners

Event listeners let JavaScript respond to user actions — clicks, keyboard presses, form submissions — by registering a callback that runs when the event fires.

Web FoundationsDialect appendices8 min read
By the end of this lesson you will be able to:
  • Attach a handler to an element with addEventListener
  • Use the event object to read target, currentTarget, and type
  • Prevent default browser behaviour with event.preventDefault
  • Explain event bubbling and use it intentionally for event delegation
  • Remove a listener correctly and explain why listener clean-up matters

The DOM becomes interactive the moment you attach an event listener. A listener is a function that the browser calls whenever a specified event — a click, a key press, a form submission — happens on a target element. Attaching one is how you cross the line from "static document" to "application."

addEventListener

element.addEventListener(type, handler) registers a function to run whenever the named event fires on that element:

const btn = document.querySelector('#submit-btn');

btn.addEventListener('click', function (event) {
  console.log('Button clicked!');
});

Common event types:

TypeFires when
'click'Mouse click or Enter on a focused element
'keydown'A key is pressed
'submit'A form is submitted
'input'The value of an input or textarea changes
'change'An input loses focus after its value changed (or a checkbox toggles)
'focus'An element receives keyboard focus
'blur'An element loses keyboard focus

Arrow functions work equally well and are more common in modern code:

btn.addEventListener('click', (event) => {
  console.log('Clicked at', event.clientX, event.clientY);
});

The event object

The browser passes an event object as the first argument to your handler. Three properties cover most needs:

  • event.target — the element the user actually interacted with (e.g. the button that was clicked).
  • event.currentTarget — the element the listener is attached to. These differ when an event bubbles; see below.
  • event.type — the event name as a string ('click', 'keydown', etc.).
document.querySelector('ul').addEventListener('click', (event) => {
  console.log('target:', event.target);          // the <li> that was clicked
  console.log('currentTarget:', event.currentTarget); // the <ul>
});

event.preventDefault

Browsers have built-in behaviour for many events: links navigate, forms submit and reload the page, right-clicking opens a context menu. Call event.preventDefault() to stop that built-in behaviour and take over:

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

form.addEventListener('submit', (event) => {
  event.preventDefault();  // stop the page reload
  // Now handle the submission in JavaScript
  const data = new FormData(form);
  console.log(data.get('email'));
});
const link = document.querySelector('a.ajax-link');

link.addEventListener('click', (event) => {
  event.preventDefault(); // stop navigation
  loadPageContent(link.href); // fetch content instead
});

Bubbling and event delegation

When an event fires, it does not stay on the target element. It bubbles up through every ancestor — the target's parent, its grandparent, all the way to document. Each ancestor's listeners for that event type also run.

<ul id="task-list">
  <li>Buy milk</li>
  <li>Call dentist</li>
</ul>
document.querySelector('#task-list').addEventListener('click', (event) => {
  // This fires even if the user clicks a <li> — because the click bubbles up
  console.log('You clicked:', event.target.textContent);
});

This pattern — attaching one listener to a parent instead of individual listeners to every child — is called event delegation. It is especially valuable when child elements are created dynamically:

// Instead of this (must re-attach every time a new item is added):
document.querySelectorAll('li').forEach(li => {
  li.addEventListener('click', handleItemClick);
});

// Do this — works for items added later too:
document.querySelector('#task-list').addEventListener('click', (event) => {
  if (event.target.tagName === 'LI') {
    handleItemClick(event.target);
  }
});

To stop an event from reaching ancestor listeners, call event.stopPropagation(). Use it sparingly — it can make debugging confusing because listeners higher up the tree silently stop receiving events they expect.

Removing listeners

element.removeEventListener(type, handler) detaches a previously attached listener. The catch: you must pass the exact same function reference you used when adding it. An anonymous function cannot be removed because there is no reference to it:

// This handler CAN be removed because we have a reference
function handleClick(event) {
  console.log('clicked');
}
btn.addEventListener('click', handleClick);
btn.removeEventListener('click', handleClick); // works

// This handler CANNOT be removed
btn.addEventListener('click', (event) => {
  console.log('clicked');
});
// There is no way to pass "the same" arrow function to removeEventListener

If you remove an element from the DOM and also let go of all JavaScript references to it, the browser's garbage collector will clean up both the element and its listeners. The memory leak scenario arises when you remove the element visually but keep a const reference to it somewhere — the element stays in memory, listeners included.

Where to go next

You understand how to listen for events. Working with Forms builds directly on addEventListener and event.preventDefault to intercept submissions, validate inputs, and give users feedback without a page reload.

Finished reading? Mark it complete to track your progress.

On this page