JavaScript addEventListener (DOM events, options, and examples)

JavaScript addEventListener on document, window, and elements: click, capture vs bubble, removeEventListener, once, passive, AbortSignal, delegation, and Node EventEmitter. DOM snippets use `{run=false}` (install jsdom or use DevTools); short notes describe expected console order.

Published

Updated

Read time 6 min read

Reviewed byDeepak Prasad

JavaScript addEventListener (DOM events, options, and examples)

addEventListener is the standard way to register a listener on any EventTarget in the browser—most often a DOM element, but also document, window, and XMLHttpRequest.

Compared with HTML one-shot handlers (onclick="..."), addEventListener lets you attach multiple listeners, choose capture vs bubbling, and use modern options such as once, passive, and signal (see the event bubbling guide on this site). The listener argument is a function—review optional parameters and arrow functions if you are new to callback style. Examples import jsdom; the public Run control does not install it, so blocks use {run=false}—run with npm install jsdom@24.0.0 and node, or paste into DevTools.

Tested on: Node.js v20.18.2; DOM snippets use jsdom 24.0.0. A short note after each snippet describes the console output.


Quick reference

Registration, one-shot listeners, passive scrolling hints, and teardown all go through the same addEventListener surface—pick the row that matches your goal.

Need API / pattern
Register listener target.addEventListener(type, listener, options?)
Run once { once: true }
Detach without keeping fn ref AbortController + { signal }
Remove explicitly removeEventListener with the same function reference

Syntax

Overload shapes

The API is usually documented in three equivalent shapes:

  • target.addEventListener(type, listener);
  • target.addEventListener(type, listener, options);
  • target.addEventListener(type, listener, useCapture); — legacy boolean capture flag

Parameters

  • type: event name string, e.g. "click", "keydown", "change" (no on prefix).
  • listener: function or object with handleEvent. The browser passes an Event instance (or subtype such as MouseEvent, KeyboardEvent).
  • Third argument: either legacy useCapture (true = capture phase, false = bubble, default), or an options object: capture, once, passive, signal.

DOM behavior with jsdom

One jsdom script exercises click, multiple listeners, delegation, stopPropagation, CustomEvent, { once: true }, removeEventListener, and AbortSignal—run with Node.js v20.18.2 and jsdom 24.0.0 (npm install jsdom@24.0.0, then node with ES modules).

javascript
import { JSDOM } from "jsdom";

const dom = new JSDOM(`<!DOCTYPE html><html><body>
<button id="myButton"></button>
<div id="parent"><div id="child"></div></div>
<ul id="list"><li>a</li><li>b</li></ul>
<div id="box"></div>
</body></html>`);
const { document, MouseEvent, CustomEvent, AbortController } = dom.window;

document.getElementById("myButton").addEventListener("click", () => console.log("click:ok"));
document.getElementById("myButton").dispatchEvent(new MouseEvent("click", { bubbles: true }));

const b2 = document.createElement("button");
document.body.appendChild(b2);
b2.addEventListener("click", () => console.log("multi:first"));
b2.addEventListener("click", () => console.log("multi:second"));
b2.dispatchEvent(new MouseEvent("click", { bubbles: true }));

const list = document.getElementById("list");
list.addEventListener("click", (e) => {
  if (e.target.tagName === "LI") console.log("delegate:" + e.target.textContent);
});
list.querySelectorAll("li")[1].dispatchEvent(new MouseEvent("click", { bubbles: true }));

const parent = document.getElementById("parent");
const child = document.getElementById("child");
parent.addEventListener("click", () => console.log("parent-should-not-run"));
child.addEventListener("click", (e) => {
  e.stopPropagation();
  console.log("child-only");
});
child.dispatchEvent(new MouseEvent("click", { bubbles: true }));

const box = document.getElementById("box");
box.addEventListener("mine", (e) => console.log("custom:" + e.detail.k));
box.dispatchEvent(new CustomEvent("mine", { detail: { k: "v" } }));

const b3 = document.createElement("button");
document.body.appendChild(b3);
let c = 0;
b3.addEventListener(
  "click",
  () => {
    c++;
    console.log("once-call");
  },
  { once: true },
);
b3.dispatchEvent(new MouseEvent("click", { bubbles: true }));
b3.dispatchEvent(new MouseEvent("click", { bubbles: true }));
console.log("once-count=" + c);

const b4 = document.createElement("button");
document.body.appendChild(b4);
function h() {
  console.log("BAD");
}
b4.addEventListener("click", h);
b4.removeEventListener("click", h);
b4.dispatchEvent(new MouseEvent("click", { bubbles: true }));
console.log("removed:ok");

const b5 = document.createElement("button");
document.body.appendChild(b5);
const ac = new AbortController();
b5.addEventListener("click", () => console.log("signal-fired"), { signal: ac.signal });
b5.dispatchEvent(new MouseEvent("click", { bubbles: true }));
ac.abort();
b5.dispatchEvent(new MouseEvent("click", { bubbles: true }));
console.log("signal:ok");
text
click:ok
multi:first
multi:second
delegate:b
child-only
custom:v
once-call
once-count=1
removed:ok
signal-fired
signal:ok

You should see 11 lines of output, starting with click:ok and ending with signal:ok.

Use querySelector when you select by CSS selector; getElementById is fine for stable ids.


Capture phase vs bubbling phase

Capture listeners run down the tree toward the target; bubble listeners run up afterward. The snippet prints the order for one click on a nested div.

Diagram: nested DOM nodes with capture listeners firing top-down then bubble listeners firing bottom-up from the event target

javascript
import { JSDOM } from "jsdom";
const dom = new JSDOM(
  '<!DOCTYPE html><html><body><div id="outer"><div id="inner"></div></div></body></html>',
);
const { document, MouseEvent } = dom.window;
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
outer.addEventListener("click", () => console.log("outer-bubble"), false);
inner.addEventListener("click", () => console.log("inner-target"), false);
outer.addEventListener("click", () => console.log("outer-capture"), true);
inner.addEventListener("click", () => console.log("inner-capture"), true);
inner.dispatchEvent(new MouseEvent("click", { bubbles: true }));
text
outer-capture
inner-capture
inner-target
outer-bubble

You should see 4 lines: outer-capture, then inner-capture, then inner-target, then outer-bubble.


preventDefault on submit

javascript
import { JSDOM } from "jsdom";
const dom = new JSDOM(
  '<!DOCTYPE html><html><body><form id="f"></form></body></html>',
);
const { document, Event } = dom.window;
const form = document.getElementById("f");
form.addEventListener("submit", (e) => {
  e.preventDefault();
});
const ev = new Event("submit", { bubbles: true, cancelable: true });
form.dispatchEvent(ev);
console.log("defaultPrevented=" + ev.defaultPrevented);
text
defaultPrevented=true

You should see one line like: defaultPrevented=true.


keydown and event.key (Enter)

Prefer event.key === "Enter" over legacy keyCode.

javascript
import { JSDOM } from "jsdom";
const dom = new JSDOM('<!DOCTYPE html><html><body><input id="i"></body></html>');
const { document, KeyboardEvent } = dom.window;
const input = document.getElementById("i");
input.addEventListener("keydown", (event) => {
  console.log("key=" + event.key + " code=" + event.code);
});
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true }));
text
key=Enter code=Enter

You should see one line like: key=Enter code=Enter.


mouseover and change (common UI events)

mouseover (often what people mean by addEventListener hover style behavior) and change on a <select>:

javascript
import { JSDOM } from "jsdom";
const dom = new JSDOM(
  `<!DOCTYPE html><html><body>
<div id="d">hi</div>
<select id="mySelect"><option value="o1">A</option><option value="o2">B</option></select>
<p id="p"></p>
</body></html>`,
);
const { document, MouseEvent, Event } = dom.window;
const d = document.getElementById("d");
d.addEventListener("mouseover", function () {
  this.style.opacity = "0.5";
});
d.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
console.log("opacity=" + d.style.opacity);
const sel = document.getElementById("mySelect");
const p = document.getElementById("p");
sel.addEventListener("change", function () {
  p.textContent = "Selected value: " + this.value;
});
sel.value = "o2";
sel.dispatchEvent(new Event("change", { bubbles: true }));
console.log(p.textContent);
text
opacity=0.5
Selected value: o2

You should see 2 lines: opacity=0.5, then Selected value: o2.

More pointer vocabulary (click, mousemove, …) is summarized in the JavaScript mouse events article.


window.addEventListener and document.addEventListener

window.addEventListener("load", …), "resize", "scroll", and document.addEventListener("DOMContentLoaded", …) use the same API on global EventTarget objects. Scroll and touch listeners are where { passive: true } most often matters for performance (see MDN and the WICG passive explainer).


Node.js is different: EventEmitter, not DOM

Node does not ship a DOM on EventTarget the same way browsers do for HTML. Typical Node code uses require("events").EventEmitter (or import of the same) with on / emit, not addEventListener on elements.

javascript
import { EventEmitter } from "node:events";

class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on("event", function () {
  console.log("An event occurred!");
});
myEmitter.emit("event");
text
An event occurred!

You should see one line like: An event occurred!.


Summary

addEventListener is the portable contract between your UI logic and the browser’s event system—master capture vs bubble, defaults like passive, and how you detach.

  • addEventListener(type, listener, options?) registers on an EventTarget; use capture vs bubble options to control phase order.
  • Prefer event.key, { once: true }, { passive: true } when appropriate; use AbortController or removeEventListener (same function reference) to detach.
  • jsdom reproduces DOM dispatch for tests; Node apps use EventEmitter instead of element listeners.
  • Delegation (e.target checks) keeps few listeners on large lists.

References

MDN and WICG documentation for addEventListener, removal, passive listeners, and event fundamentals.

EventTarget: addEventListener() - MDN
EventTarget: removeEventListener() - MDN
Introduction to events - MDN
Passive event listeners explainer (WICG)


Frequently Asked Questions

1. What does document.getElementById("myButton").addEventListener("click", function() { alert("button clicked!"); }); do?

It registers a click listener on the existing element with id myButton. When the user clicks that button, the browser runs the function (here showing an alert). It does not create the button, change its label, or remove it from the page.

2. What is the difference between addEventListener and onclick in HTML?

Inline onclick attaches one handler string per attribute and mixes markup with behavior. addEventListener is called from JavaScript, supports multiple listeners, the options object (once, passive, capture, signal), and removeEventListener for the same function reference.

3. Can you remove an anonymous or arrow function listener?

Not with removeEventListener unless you keep the same function reference in a variable. Use a named function, store the arrow in a const, or use AbortController.signal to detach without keeping the function reference.

4. What does passive true mean on addEventListener?

The listener promises not to call preventDefault, so the browser can optimize scrolling or touch defaults. If you still call preventDefault, it is ignored and the browser may warn.

5. What was addeventlistener Moz in old browsers?

Gecko exposed mozAddListener / prefixed variants before standard addEventListener was universal. Modern code should use addEventListener on EventTarget.

6. Is mouseover the same as hover in JavaScript?

CSS uses :hover; in JS, mouseover and mouseout fire when the pointer enters or leaves the element (and can repeat for child elements). mouseenter and mouseleave do not bubble from children; pick the pair that matches your UX.
Olorunfemi Akinlua

Boasting over five years of experience in JavaScript, specializing in technical content writing and UX design. With a keen focus on programming languages, he crafts compelling content and designs …