DOM Events
An event is an object that the browser dispatches to an element when
something happens: a click, a key press, a finished load. It travels in three
phases: capturing (from window down to the target), target, and
bubbling (back up to window). Handlers run in the bubbling phase by
default. Event delegation uses bubbling: one handler on an ancestor serves
all its descendants.
Details
Adding and Removing Handlers
button.addEventListener('click', handleClick, options);
button.removeEventListener('click', handleClick, options);
removeEventListenerneeds the same function reference and the samecapturevalue. An inline arrow function or a newhandler.bind(this)cannot be removed this way.optionsis an object, or a boolean that meanscapture:
| Option | Meaning |
|---|---|
capture | run the handler in the capturing phase |
once | remove the handler after the first call |
passive | the handler promises not to call preventDefault(), so scrolling does not wait for it |
signal | an AbortSignal; controller.abort() removes the handler |
touchstart, touchmove and wheel listeners on window, document and
<body> are passive by default; to call preventDefault() in them, pass
{ passive: false }.
Propagation
event.targetis the element where the event happened;event.currentTargetis the element whose handler is running now.- Order for a click on a nested element: capturing handlers from the outermost ancestor down, handlers of the target, bubbling handlers from the parent up.
- Some events do not bubble:
focus/blur(the bubbling versions arefocusin/focusout),mouseenter/mouseleave(mouseover/mouseout),loadanderrorof resources. They can still be caught on an ancestor in the capturing phase.
Stopping an Event
| Method | What it does |
|---|---|
event.preventDefault() | cancels the default action (following a link, submitting a form); the event keeps propagating |
event.stopPropagation() | stops the event from reaching other elements; other handlers on the current element still run |
event.stopImmediatePropagation() | the same, and also skips the remaining handlers on the current element |
Common Events
| Group | Events |
|---|---|
| Mouse | click, dblclick, mousedown, mouseup, mousemove, mouseover, mouseout |
| Pointer | pointerdown, pointerup, pointermove: one API for mouse, touch and pen |
| Keyboard | keydown, keyup |
| Form | submit, input, change, focus, blur |
| Page | DOMContentLoaded, load, resize, scroll, visibilitychange, pagehide |
Deprecated: keypress (use keydown, or input for text) and unload (it is
unreliable on mobile, disables the back/forward cache, and Chrome is phasing it
out; use visibilitychange or pagehide).
Example
Delegation: one handler for all items, including those added later.
<ul id="menu">
<li data-action="save"><span>Save</span></li>
<li data-action="delete"><span>Delete</span></li>
</ul>
const menu = document.getElementById('menu');
menu.addEventListener('click', (event) => {
// event.target can be the <span>, so matches('[data-action]') would fail.
// closest() checks the element itself and then its ancestors.
const item = event.target.closest('[data-action]');
if (!item || !menu.contains(item)) return;
console.log(item.dataset.action); // 'save' or 'delete'
});
Follow-up Questions
- Why use delegation? There is one handler instead of many, and it works for elements added after the handler was attached.
- How do you remove several handlers at once? Pass the same
signalto all of them and callcontroller.abort(). - How does React attach handlers? It delegates: one listener per event type on the root container, not on every element.
Common Mistakes
- Mixing up
preventDefault()andstopPropagation(): the first is about the browser's default action, the second is about other handlers. - Trying to remove a handler by passing a new function that looks the same.
- Using
event.target.matches()in delegation: it fails when the click lands on a nested element.