A button does not “run JavaScript” by itself. The browser first detects an interaction, creates an event object, sends that event through the relevant DOM path, and invokes any listeners that match the event type. Your code then decides whether to update the page, submit data, ignore the interaction, or stop a default action.
This chain explains many beginner bugs. A click listener may be attached to the wrong element. A form may reload because its default submission was never canceled. A parent menu may react when a child button was clicked because the event bubbled upward. Understanding the chain is more useful than memorizing isolated snippets.
What an Event Actually Represents
An event is a signal that something happened in the environment where a program is running. In a browser, that might be a user clicking, pressing a key, entering text, submitting a form, resizing the window, finishing a page load, or triggering an error. The event itself is an object with information about what happened and where it happened.
Events belong to the browser’s Web APIs rather than to the core syntax of the JavaScript language. That distinction matters because the browser decides when to dispatch a click or submit event, while your JavaScript registers the function that should respond. The DOM guide on Vandutz is useful background here: the browser represents the document as a tree, and events can travel through that tree. The WHATWG DOM Standard defines the underlying event interfaces and dispatch model used by the Web platform.
A simple interaction begins with HTML:
<button id="save-button" type="button">Save draft</button>
<p id="status" aria-live="polite"></p>JavaScript can listen for the button’s click without placing code inside the HTML:
const saveButton = document.querySelector("#save-button");
const status = document.querySelector("#status");
function showSavedMessage() {
status.textContent = "Draft saved.";
}
saveButton.addEventListener("click", showSavedMessage);When the user clicks, the browser dispatches a click event and calls showSavedMessage. The listener does not run when the file is parsed. It waits for the event.
Why addEventListener Is the Safer Starting Point
addEventListener() registers a function for a particular event type. MDN recommends it because it supports more than one listener on the same target, works with different event phases, and provides options such as once, passive, capture, and signal [1].
function recordClick() {
console.log("The button was clicked");
}
saveButton.addEventListener("click", recordClick);
saveButton.addEventListener("click", showSavedMessage);Both functions can respond to the same click. By contrast, assigning saveButton.onclick repeatedly replaces the previous handler. Inline attributes such as <button onclick="save()"> mix behavior into markup, become difficult to maintain across many elements, and are discouraged by MDN [1]. Keeping the listener in JavaScript also makes the relationship easier to inspect and test.
Function identity matters when removing a listener:
saveButton.removeEventListener("click", recordClick);This works because recordClick is the same function reference that was registered. Creating a new anonymous function inside removeEventListener() does not identify the old listener. In larger interfaces, keeping named handlers or an intentional lifecycle strategy prevents old behavior from accumulating after a component is removed or rebuilt.
The Event Object Separates Target From Handler
Every event listener receives an event object. Two properties are especially important. event.target is the element where the event originally occurred. event.currentTarget is the element whose listener is currently running.
const panel = document.querySelector("#settings-panel");
panel.addEventListener("click", (event) => {
console.log("Original element:", event.target);
console.log("Element handling it:", event.currentTarget);
});If a user clicks a <span> inside the panel, target may be that span while currentTarget is the panel. If the listener is attached directly to the span, the two references may point to the same element. Confusing them is a common source of code that changes the wrong component.
The event object also exposes the event type, whether it bubbles, whether it can be canceled, and methods such as preventDefault() and stopPropagation(). The MDN reference for addEventListener() documents how the listener callback receives this object and how listener options affect behavior [2].
Click, Input, and Submit Are Different Questions
Choosing the event type should follow the user action you need to observe. A click usually describes activation of a button or link. An input event reports that the value of a text field changed as the user edits it. A submit event belongs to the form submission process and is often the right place for validation before data is sent.
const nameField = document.querySelector("#name");
const preview = document.querySelector("#preview");
nameField.addEventListener("input", (event) => {
preview.textContent = `Hello, ${event.target.value}`;
});Using input instead of click makes the intent clear: the preview should change as the value changes, not only when the user clicks somewhere. For keyboard accessibility, avoid designing a critical action around a mouse-only event when a semantic button, form, or input already provides the browser with useful keyboard behavior.
A form example shows why submit is often more robust than listening only for a button click:
const form = document.querySelector("#profile-form");
const email = document.querySelector("#email");
const message = document.querySelector("#form-message");
form.addEventListener("submit", (event) => {
if (!email.value.includes("@")) {
event.preventDefault();
message.textContent = "Enter a valid email address.";
}
});The form can be submitted by clicking its button or by using the keyboard. Handling the form’s submit event covers both paths. preventDefault() cancels the browser’s normal submission in this example; it does not stop the event from traveling to ancestors.
Default Action and Propagation Are Not the Same
Two event methods are frequently mixed up. preventDefault() tells the browser not to perform the event’s default action when the event is cancelable. For a form, that can stop navigation or network submission. For a link, it can stop navigation. It does not prevent other listeners from receiving the event.
stopPropagation() affects the event’s journey through the DOM tree. It can stop the event from reaching another object higher in the path, but it does not automatically cancel the browser’s default action. The practical question is therefore different:
| Problem | Likely tool | What it changes |
|---|---|---|
| The form navigates before validation finishes | event.preventDefault() | Cancels the default submit action when allowed |
| A child click also triggers a parent panel handler | event.stopPropagation() | Stops the event from continuing to other objects |
| Later listeners on the same object must not run | event.stopImmediatePropagation() | Stops later listeners and further propagation |
| A scroll listener should never cancel scrolling | { passive: true } | Declares that the listener will not call preventDefault() |
Use these methods narrowly. If a parent should ignore a child interaction, checking the target or changing the component structure may be clearer than stopping propagation everywhere. An application that calls stopPropagation() on every event can make other listeners unexpectedly impossible to compose.
Bubbling Lets One Listener Handle Many Children
When an event occurs on a nested element, it may bubble from the target toward its ancestors. MDN’s bubbling guide shows a click reaching the button first, then its parent, and then the body [3]. This is why a listener on a list can respond to clicks on many list items without registering a separate listener for every item.
const list = document.querySelector("#task-list");
list.addEventListener("click", (event) => {
const deleteButton = event.target.closest("[data-delete]");
if (!deleteButton) return;
const item = deleteButton.closest("li");
item.remove();
});This pattern is called event delegation. It is especially useful when list items are created after the initial page load. The listener is attached to a stable ancestor, and event.target helps identify the descendant that the user actually activated. Use closest() carefully and confirm that the returned element belongs to the expected container.
Bubbling can also produce surprising results. If a card has a click handler that opens details and a button inside the card has a click handler that deletes the card, the delete action may also open the details panel. In that case, either define the interaction so both outcomes are intentional or stop propagation in the delete handler after considering the accessibility and composition consequences.
Capture Runs Before the Target
Event propagation can also use a capture phase. With { capture: true }, a listener on an ancestor runs while the event travels toward the target, before the target’s normal handler. The default for most listeners is the bubbling phase, which is usually the simpler choice.
document.body.addEventListener("click", () => {
console.log("Body capture");
}, { capture: true });
saveButton.addEventListener("click", () => {
console.log("Button target");
});Capture can be useful for instrumentation, global interaction rules, or cases where an ancestor must observe an event before a descendant handles it. It is not a general fix for a listener that is attached to the wrong element. Start with the normal target and bubbling model, then add capture for a specific reason.
A Debugging Routine That Follows the Event
- Confirm the element exists. Log the result of
querySelector()before callingaddEventListener(). A script loaded before the markup may receivenull. - Confirm the event name. Check whether the action is a click, input, change, submit, keydown, or another event. Similar words do not make interchangeable events.
- Log both targets. Print
event.targetandevent.currentTargetto see whether delegation or bubbling is involved. - Check the default action. If the page navigates or the form reloads, decide whether
preventDefault()belongs in the handler. - Check the propagation path. If another component reacts unexpectedly, inspect parent listeners before adding
stopPropagation(). - Test keyboard and dynamic content. Activate controls with the keyboard and create new elements after load to see whether the chosen listener still covers the real interface.
For a broader foundation, compare this article with Vandutz’s guides to JavaScript functions and if/else decisions. Event handling becomes easier to read when the listener function has one clear job and the branching inside it reflects the possible user actions.
The durable mental model is simple: the browser dispatches a signal, a listener receives an event object, the event may travel through the DOM, and the handler may update application state or cancel a default action. Once you separate those steps, click, input, and submit stop looking like mysterious magic and become ordinary parts of a predictable interface.
Build the next interaction with stronger JavaScript fundamentals

Alex Carter is the editorial name behind Vandutz Academy, a programming blog for beginners. Alex reviews and tests the examples and explanations published on the site, with a focus on making Python, JavaScript, web development, and developer tools easier to understand.