The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Register a listenerbutton.addEventListener('click', handleClick)View examples
Listen oncebutton.addEventListener('click', initialize, { once: true })View examples
Group listener cleanuppanel.addEventListener('input', update, { signal: controller.signal })View examples
Remove a listenerbutton.removeEventListener('click', handleClick)View examples
Observe the capture phasedocument.addEventListener('click', inspect, { capture: true })View examples
Read the listener targetconst container = event.currentTargetView examples
Stop further propagationevent.stopPropagation()View examples
Resolve a delegated actionconst action = event.target instanceof Element ? event.target.closest('[data-action]') : nullView examples
Verify delegation ownershipif (!menu.contains(action)) returnView examples
Cancel a default actionif (event.cancelable) event.preventDefault()View examples
Declare a passive listenerwindow.addEventListener('touchmove', observe, { passive: true })View examples
Inspect cancellationif (event.defaultPrevented) returnView examples
Create an application eventconst event = new CustomEvent('cart:add', { detail: { sku: 'A42' } })View examples
Dispatch synchronouslyconst accepted = cart.dispatchEvent(event)View examples
Inspect the event pathconst path = event.composedPath()View examples
Cross a shadow boundarynew CustomEvent('widget:change', { bubbles: true, composed: true })View examples
Prefer activation eventsbutton.addEventListener('click', activate)View examples
Read a logical keyif (event.key === 'Escape') closeDialog()View examples

Browser events connect user actions and platform state to application behavior. Robust event code distinguishes the original target from the registered listener, delegates only from a verified ancestor, cancels default actions only when the event permits it, and gives every long-lived listener an explicit lifetime. Shadow DOM adds retargeting and composed boundaries, so inspect the event path when component internals matter instead of assuming target exposes the deepest node.

Step by step

Detailed examples

01

Register listeners with an explicit lifetime

addEventListener keeps a callback until it is removed, invoked with once, or detached through an AbortSignal. Removal matches the event type, callback identity, and capture flag; a newly created function is not the callback that was registered. AbortController is useful for components because one abort can remove several listeners without maintaining parallel callback bookkeeping.

Dispose all listeners owned by a mounted panel
<section id="filters">
  <input aria-label="Filter products">
  <button type="button">Apply once</button>
</section>
<script>
  const panel = document.querySelector('#filters');
  const controller = new AbortController();
  const options = { signal: controller.signal };

  panel.addEventListener('input', updateResults, options);
  panel.querySelector('button').addEventListener('click', apply, { ...options, once: true });

  function updateResults(event) { console.log(event.target.value); }
  function apply() { console.log('Applied'); }

  // Run when the panel is unmounted:
  // controller.abort();
</script>
Back to quick reference ↑
02

Distinguish the target, phase, and current listener

Dispatch builds an event path and normally invokes capturing listeners from ancestors toward the target, target listeners, then bubbling listeners from the target outward when bubbles is true. event.target identifies the retargeted origin for the current listener; event.currentTarget is the object whose callback is executing. stopPropagation prevents traversal to later nodes, while stopImmediatePropagation also blocks later listeners on the current node. Neither method cancels a default action.

Log capture and bubble order
<div id="outer"><button id="save">Save</button></div>
<script>
  const outer = document.querySelector('#outer');
  const save = document.querySelector('#save');

  outer.addEventListener('click', log, { capture: true });
  save.addEventListener('click', log);
  outer.addEventListener('click', log);

  function log(event) {
    console.log(event.eventPhase, event.target.id, event.currentTarget.id);
  }
</script>
Back to quick reference ↑
03

Delegate from a stable ancestor and verify the match

Delegation attaches one bubbling listener to a stable ancestor and resolves the action from the event target exposed to that listener. closest handles clicks on nested icons or labels, but it can return a matching ancestor beyond the intended subtree. Verify ownership with contains, encode behavior with data attributes, and let native controls provide keyboard activation and semantics. Events that do not bubble need a bubbling alternative or a capture listener.

Handle current and future toolbar buttons
<div id="toolbar" role="toolbar" aria-label="Text style">
  <button type="button" data-action="bold"><strong>Bold</strong></button>
  <button type="button" data-action="italic"><em>Italic</em></button>
</div>
<script>
  const toolbar = document.querySelector('#toolbar');
  toolbar.addEventListener('click', (event) => {
    if (!(event.target instanceof Element)) return;
    const control = event.target.closest('button[data-action]');
    if (!control || !toolbar.contains(control)) return;
    console.log(`Apply ${control.dataset.action}`);
  });
</script>
Back to quick reference ↑
04

Cancel behavior, not propagation, when replacing a default

preventDefault signals that a cancelable event's default action should not run; it does not stop other listeners. Check cancelable when code handles multiple event sources, and use defaultPrevented when cooperating listeners need to honor an earlier decision. A passive listener cannot cancel through preventDefault. Cancel native behavior only when the replacement preserves expected keyboard, focus, navigation, and accessibility behavior.

Validate a form before allowing submission
<form id="profile">
  <label>Name <input name="name" required></label>
  <button>Save</button>
</form>
<p id="message" role="status"></p>
<script>
  const form = document.querySelector('#profile');
  const input = form.elements.name;
  const message = document.querySelector('#message');
  input.addEventListener('input', () => input.setCustomValidity(''));
  form.addEventListener('submit', (event) => {
    if (input.value.trim().length >= 2) return;
    if (event.cancelable) event.preventDefault();
    input.setCustomValidity('Enter at least two characters.');
    message.textContent = 'The name is too short.';
    input.reportValidity();
  });
</script>
Back to quick reference ↑
05

Publish component outcomes with explicit custom events

CustomEvent carries application data in detail. Choose a namespaced, stable type and treat the payload as a public contract. dispatchEvent runs listeners synchronously; it returns false only when the event is cancelable and a listener canceled it. Script-dispatched events are not trusted user events. Set bubbles when ancestors should delegate the event and composed only when the contract intentionally crosses a shadow boundary.

Let a consumer veto a cart operation
const cart = document.querySelector('#cart');

cart.addEventListener('cart:add', (event) => {
  if (event.detail.quantity > 10) event.preventDefault();
});

const request = new CustomEvent('cart:add', {
  detail: { sku: 'A42', quantity: 12 },
  bubbles: true,
  cancelable: true
});

if (!cart.dispatchEvent(request)) {
  console.log('The cart rejected this quantity.');
}
Back to quick reference ↑
06

Account for retargeting across shadow trees

When an event crosses a shadow boundary, target can be retargeted to the host so that component internals are not exposed as ordinary descendants. composedPath returns the invocation path visible to the caller, excluding nodes hidden by a closed shadow root. Bubbling and composed are independent flags: a custom event commonly needs both to support delegation outside a component. Do not depend on private internal nodes as a component's public event contract.

Expose a stable change event from a custom element
class QuantityPicker extends HTMLElement {
  set value(value) {
    this._value = Number(value);
    this.dispatchEvent(new CustomEvent('quantity:change', {
      detail: { value: this._value },
      bubbles: true,
      composed: true
    }));
  }
}
customElements.define('quantity-picker', QuantityPicker);

document.addEventListener('quantity:change', (event) => {
  console.log(event.detail.value, event.composedPath()[0]);
});
Back to quick reference ↑
07

Model actions independently of a specific input device

Use semantic buttons and their click activation for actions rather than implementing the action only on pointerdown or keydown. This preserves native keyboard and assistive-technology activation. For genuinely keyboard-specific behavior, KeyboardEvent.key represents the logical key value, while code represents the physical key position. Pointer events are appropriate for position, pressure, or multi-pointer interactions, but ordinary controls should retain click as their activation contract.

Support native activation and an Escape shortcut
<button id="open" type="button">Open details</button>
<dialog id="details"><p>Details</p><button id="close" type="button">Close</button></dialog>
<script>
  const dialog = document.querySelector('#details');
  document.querySelector('#open').addEventListener('click', () => dialog.showModal());
  document.querySelector('#close').addEventListener('click', () => dialog.close());
  dialog.addEventListener('keydown', (event) => {
    if (event.key === 'Escape') console.log('The dialog will perform its native cancel behavior.');
  });
</script>
Back to quick reference ↑

Local code tester

Delegate actions from a dynamic list

Add and remove tasks while one listener on the list handles every current and future remove button.

Runs in your browser
Preview

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. WHATWGDOM Standard: Eventsdom.spec.whatwg.org
  2. WHATWGDOM Standard: Dispatching eventsdom.spec.whatwg.org
  3. World Wide Web ConsortiumUI Eventsw3.org
  4. World Wide Web ConsortiumPointer Eventsw3.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback