Events
addEventListener
The only way you should attach event handlers in modern JavaScript.
const button = document.querySelector("#save-btn");
button.addEventListener("click", function(event) {
console.log("Button clicked");
});
Never use inline handlers like <button onclick="handleClick()">. They mix behavior with markup and only allow one handler per event.
Removing Listeners
Pass the same function reference to remove a listener. Anonymous functions cannot be removed.
function handleClick(event) { console.log("Clicked"); }
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);
Options
button.addEventListener("click", handler, { once: true }); // run once
button.addEventListener("click", handler, { capture: true }); // capture phase
document.addEventListener("touchstart", handler, { passive: true }); // better scroll perf
The Event Object
Every handler receives an event object with details about what happened.
form.addEventListener("submit", function(event) {
event.preventDefault(); // stop default browser behavior
console.log(event.target); // element that caused the event
console.log(event.currentTarget); // element the listener is on
console.log(event.type); // "submit"
});
target vs currentTarget
event.target is the element the user interacted with. event.currentTarget is the element the listener is attached to. These differ when events bubble.
document.querySelector("#menu").addEventListener("click", function(event) {
console.log(event.target.tagName); // "LI" (what was clicked)
console.log(event.currentTarget.tagName); // "UL" (where listener is)
});
stopPropagation
Stops the event from traveling further. Use sparingly — it makes debugging harder.
document.querySelector(".dropdown-menu").addEventListener("click", function(event) {
event.stopPropagation(); // clicks inside menu don't close it
});
document.addEventListener("click", () => closeAllDropdowns());
Event Bubbling & Capturing
Events go through three phases: capture (down from document), target, then bubble (back up). By default, listeners fire during the bubble phase.
document.querySelector("#outer").addEventListener("click", () => console.log("outer"));
document.querySelector("#inner").addEventListener("click", () => console.log("inner"));
document.querySelector("#btn").addEventListener("click", () => console.log("button"));
// Clicking #btn: "button" → "inner" → "outer"
Event Delegation
Attach one listener to the parent instead of one per child. More efficient, and it handles dynamically added elements.
<ul id="task-list">
<li data-id="1">Task One <button class="delete">X</button></li>
<li data-id="2">Task Two <button class="delete">X</button></li>
</ul>
document.querySelector("#task-list").addEventListener("click", function(event) {
if (event.target.classList.contains("delete")) {
const taskItem = event.target.closest("li");
console.log(`Deleting task ${taskItem.dataset.id}`);
taskItem.remove();
}
});
closest() walks up the DOM to find the nearest ancestor matching a selector. It is essential for delegation because event.target might be a child of what you care about.
// Bad: one listener per item, breaks for new items
document.querySelectorAll(".item").forEach(item => {
item.addEventListener("click", handleItemClick);
});
// Good: one listener handles current and future items
document.querySelector("#item-list").addEventListener("click", function(event) {
const item = event.target.closest(".item");
if (item) handleItemClick(item);
});
Common Events
Keyboard
document.addEventListener("keydown", function(event) {
console.log(event.key); // "Enter", "a", "Escape", "ArrowUp"
if (event.ctrlKey && event.key === "s") {
event.preventDefault();
saveDocument();
}
});
Form
form.addEventListener("submit", handler); // form submitted
input.addEventListener("input", handler); // every keystroke/change
input.addEventListener("change", handler); // value changed on blur
input.addEventListener("focus", handler); // gained focus
input.addEventListener("blur", handler); // lost focus
Scroll & Resize
window.addEventListener("scroll", handler); // fires frequently — throttle in production
window.addEventListener("resize", handler); // also fires frequently
Page Lifecycle
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM ready"); // images/stylesheets may still be loading
});
window.addEventListener("load", () => {
console.log("Everything loaded");
});
Use DOMContentLoaded for initializing JavaScript. Better yet, place your <script> at the end of <body> or use the defer attribute.
Custom Events
Create your own events with CustomEvent to decouple components.
// Dispatch a custom event with data
const productList = document.querySelector("#products");
productList.dispatchEvent(new CustomEvent("product:added", {
detail: { id: 42, name: "Widget" },
bubbles: true // allows delegation
}));
// Listen for it anywhere up the tree
document.addEventListener("product:added", function(event) {
console.log("New product:", event.detail.name);
updateCartCount();
});
Custom events are useful when one part of the page needs to announce that something happened without knowing who is listening. The dispatching code and the listening code are completely independent.
Common Pitfalls
Forgetting preventDefault on form submit. The browser reloads the page by default.
Attaching listeners inside loops without delegation. Hundreds of individual listeners waste memory and miss dynamically added elements.
Overusing stopPropagation. It silently breaks delegation and analytics tracking upstream.
Not removing listeners when elements are removed. Listeners keep closures in memory. Use removeEventListener or { once: true }.
Using keyCode instead of key. event.keyCode is deprecated. Use event.key.
Key Takeaways
- Always use
addEventListener. Never inline handlers. event.targetis what was interacted with.event.currentTargetis where the listener lives.- Events bubble up from the target to the document. Use this for event delegation.
- Event delegation means one listener on the parent, not many on the children. It handles dynamic elements automatically.
- Use
preventDefaultto stop defaults. UsestopPropagationsparingly. - Custom events with
CustomEventdecouple components without direct references.