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.