The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Register a listener | button.addEventListener('click', handleClick) | View examples |
| Listen once | button.addEventListener('click', initialize, { once: true }) | View examples |
| Group listener cleanup | panel.addEventListener('input', update, { signal: controller.signal }) | View examples |
| Remove a listener | button.removeEventListener('click', handleClick) | View examples |
| Observe the capture phase | document.addEventListener('click', inspect, { capture: true }) | View examples |
| Read the listener target | const container = event.currentTarget | View examples |
| Stop further propagation | event.stopPropagation() | View examples |
| Resolve a delegated action | const action = event.target instanceof Element ? event.target.closest('[data-action]') : null | View examples |
| Verify delegation ownership | if (!menu.contains(action)) return | View examples |
| Cancel a default action | if (event.cancelable) event.preventDefault() | View examples |
| Declare a passive listener | window.addEventListener('touchmove', observe, { passive: true }) | View examples |
| Inspect cancellation | if (event.defaultPrevented) return | View examples |
| Create an application event | const event = new CustomEvent('cart:add', { detail: { sku: 'A42' } }) | View examples |
| Dispatch synchronously | const accepted = cart.dispatchEvent(event) | View examples |
| Inspect the event path | const path = event.composedPath() | View examples |
| Cross a shadow boundary | new CustomEvent('widget:change', { bubbles: true, composed: true }) | View examples |
| Prefer activation events | button.addEventListener('click', activate) | View examples |
| Read a logical key | if (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
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.
<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> 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.
<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> 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.
<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> 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.
<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> 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.
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.');
} 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.
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]);
}); 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.
<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> 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.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



