Lesson · Beginner

Handle browser events without breaking native behavior

Register event listeners, understand propagation and defaults, and connect behavior to native controls without duplicating handlers or input assumptions.

Published
Last checked

Begin with an element that already has a contract

Events report that something happened. They do not supply semantics. A native button already participates in focus order, has keyboard activation, exposes a button role and name, and supports disabled state. JavaScript should respond to that activation rather than rebuild it on a generic container.

<button type="button" id="details-toggle" aria-expanded="false">
  Show details
</button>
<div id="details" hidden>Workshop materials are included.</div>

The HTML remains understandable before behavior loads. The script enhances a real control and must keep visible state, hidden, and aria-expanded synchronized.

Register one listener

const button = document.querySelector('#details-toggle');
const details = document.querySelector('#details');

button?.addEventListener('click', () => {
  const willOpen = details.hidden;
  details.hidden = !willOpen;
  button.setAttribute('aria-expanded', String(willOpen));
  button.textContent = willOpen ? 'Hide details' : 'Show details';
});

The button's native activation produces click for pointer and keyboard use, so separate keydown code is unnecessary. Optional chaining keeps a missing optional enhancement from crashing unrelated page scripts. A required control should be validated explicitly and fail with a useful development error instead.

Understand propagation

An event is dispatched along a path. Capture listeners run on the way toward the target, target listeners run at the target, and bubbling listeners run back through ancestors when the event bubbles. event.target is the original target; event.currentTarget is the object whose listener is currently running.

Delegation uses that behavior to manage repeated current or future children from a stable ancestor. Confirm the event type bubbles, identify the closest intended control, and reject targets outside the container. Do not make the entire document one undifferentiated click router.

Stopping propagation should be rare. Another component, analytics boundary, or browser behavior may legitimately observe the same event. Use an ownership boundary or more precise selector before silencing the event path.

Preserve default actions

Links navigate, submit buttons submit forms, checkboxes toggle, and text fields accept editing commands. preventDefault() cancels an event's cancelable default action; it should appear only when the script supplies a complete replacement.

For a progressively enhanced form, prevent submission after the form's own validation and only when an asynchronous request will produce an equally honest success or failure. If initialization fails, the native action should remain. Never cancel every keydown event to implement a shortcut; respect editing, assistive technology, operating-system commands, and modifier keys.

Manage listener lifetime

Repeated initialization can attach the same logical handler several times, causing duplicate requests or state changes. Initialize once at a stable module boundary, use a named callback when it must be removed, or associate listeners with an AbortSignal.

const controller = new AbortController();

window.addEventListener('resize', handleResize, {
  signal: controller.signal,
});

// When the component is permanently removed:
controller.abort();

The once option is useful for genuinely single-use listeners. passive tells the browser that a listener will not cancel scrolling; apply it only when that promise is true. Listener options describe behavior and lifetime, not generic performance decoration.

Accessibility consequences

Do not infer input method from one event type. A click can originate from a mouse, touch, keyboard activation, voice control, or automation. Hover has no equivalent on many touch screens. Pointer coordinates do not express the intent of a keyboard user.

Focus events need equal care. Merely receiving focus must not unexpectedly submit a form, open a new window, or move the user elsewhere. When a dialog or error summary creates a new task context, move focus deliberately and restore it when the context closes. Keep a visible focus indicator throughout.

Common failures

  • A generic node listens for click but cannot be focused or activated by keyboard.
  • Initialization runs after every navigation and duplicates listeners.
  • A document listener handles events from an unrelated component.
  • preventDefault() disables navigation or form submission before the enhancement is ready.
  • A key handler intercepts typing, browser shortcuts, or assistive-technology commands.

Verify and practice

Use the control with pointer, Tab, Enter, Space, touch, and voice input where available. Initialize the script twice and confirm one action still produces one result. Remove the component and check that global listeners and observers stop. Force an exception before listener registration and confirm the native baseline remains usable.

For practice, implement the disclosure above, then add three disclosures through delegation on their shared container. Log target, currentTarget, event phase, and defaultPrevented. Remove the logs after you can predict the sequence. Test the finished pattern at 320 pixels, 200% zoom, and with a screen reader without adding custom keyboard behavior to the native button.

Free template + launch checklist

Build a page you can check before launch

Start with one reviewed semantic HTML page, then work through the one-page checklist against your published URL.

Check your inbox and confirm before the download arrives. No account required. We do not track email opens. See the privacy notice.