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.
- 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:
| Type | Fires 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 removeEventListenerIf 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.
Reading and Writing Content
JavaScript can read and modify the text content, HTML structure, and attributes of any element in the DOM.
Working with Forms
The submit event, preventDefault, and FormData together let JavaScript intercept a form submission, read all its values, and decide what to do with them.