Events & delegation
The event object, bubbling and capturing, delegation, removing listeners and keyboard events.
Events are how the browser tells your code that something happened: a click, a key press, a form submission, the page finishing loading. In this lesson you'll learn the event object, how events travel through the DOM, and the delegation pattern used by every serious front-end codebase.
Listening and the event object#
Every handler receives an event object with details about what happened. Its properties depend on the event type:
Common event types
- Mouse/pointer:
click,dblclick,pointerdown,pointerup,pointermove,pointerenter,pointerleave,contextmenu - Keyboard:
keydown,keyup - Form:
input(every change),change(committed change),submit,focus,blur,focusin,focusout - Page/window:
DOMContentLoaded,load,resize,scroll,visibilitychange,beforeunload
Prefer pointer events over mouse events — they work for mouse, touch and pen.
Keyboard events#
event.key is the character or key name ("a", "A", "Enter", "ArrowUp"); event.code is the physical key ("KeyA"), which ignores keyboard layout. Use key for shortcuts and text, code for games.
Preventing default behaviour#
Some events have built-in browser behaviour: links navigate, forms submit and reload the page, Ctrl+S saves. event.preventDefault() cancels it:
How events travel: capture, target, bubble#
When you click a <span> inside a <button> inside a <li>, the event doesn't just happen on the span. It travels in three phases:
- Capture — from
windowdown through each ancestor to the target. - Target — at the element that was clicked.
- Bubble — back up from the target through each ancestor to
window.
Listeners run in the bubble phase by default:
Clicking "Home" logs:
target vs currentTarget
Stopping propagation
event.stopPropagation() stops the event travelling further. Use it sparingly — it breaks delegation and analytics code higher up that expects to see the event. There's usually a better design.
Event delegation#
Instead of adding a listener to every item — including ones created later — add one listener to a common parent and figure out which child was involved:
Why delegation is great:
- One listener instead of hundreds — less memory, faster setup.
- Dynamic content just works — no need to attach listeners to new elements.
- Pairs perfectly with
data-*attributes to identify items.
closest(selector) walks up from the clicked element (the target could be a nested icon or text) to the element you care about.
Listener options#
once: true— the listener removes itself after the first call.passive: true— promises you won't callpreventDefault(), letting the browser scroll smoothly.capture: true— listen during the capture phase.signal— remove the listener with anAbortController(below).
Removing listeners#
To remove a listener you need the same function reference:
To remove several at once, use an AbortController:
Throttling busy events#
scroll, resize, pointermove and input can fire dozens of times per second. Debouncing waits until the events stop:
Now the search only runs 400 ms after the user stops typing. (This debounce is a closure — the timer variable is remembered between calls.)
Custom events#
Components can announce their own events:
Waiting for the page#
With defer or type="module" scripts, the DOM is ready when your code runs. For inline scripts in <head>, wait for DOMContentLoaded:
Common mistakes#
- Writing
button.addEventListener("click", handleClick())— that callshandleClickimmediately. PasshandleClick. - Using
event.targetwhen the click landed on a nested element — useclosest(). - Adding listeners inside a function that runs repeatedly, stacking duplicates.
- Overusing
stopPropagation(). - Relying on click handlers on
<div>s — use real<button>s so keyboard and screen-reader users can activate them.
What's next#
Next: forms and validation — reading user input, the browser's built-in validation and writing your own checks.
Check your understanding
Quick quiz
1.In event delegation, where do you attach the listener?
2.What is the difference between
event.targetandevent.currentTarget?3.Why does
button.removeEventListener("click", () => save())fail to remove a listener added withaddEventListener("click", () => save())?
Finished reading?
Mark this lesson complete to track your progress.