Events are actions that happen in the browser — clicks, key presses, form submissions, page load, and more. JavaScript responds to events with event listeners (handlers).

Adding Event Listeners

  const button = document.querySelector('#myBtn');

button.addEventListener('click', function() {
    console.log('Button clicked!');
});

button.addEventListener('click', () => {
    console.log('Clicked with arrow function');
});
  

Avoid inline handlers like onclick="..." in HTML — they mix concerns, block CSP, and are harder to maintain.

The Event Object

Handlers receive an event object with useful properties and methods:

  button.addEventListener('click', (event) => {
    console.log(event.type);       // 'click'
    console.log(event.target);     // element that triggered the event
    console.log(event.currentTarget); // element listener is attached to
    event.preventDefault();        // cancel default (form submit, link navigation)
    event.stopPropagation();       // stop bubbling to parents
    event.stopImmediatePropagation(); // stop other listeners on same element
});
  

Common Event Types

Event Fires when
click Element clicked
dblclick Double-click
mousedown / mouseup Mouse button pressed/released
mouseenter / mouseleave Mouse enters/leaves (no bubble)
mouseover / mouseout Mouse enters/leaves (bubbles)
keydown / keyup / keypress Key events
input Input value changes (every keystroke)
change Value committed (select, checkbox, blur after edit)
submit Form submitted
focus / blur Element gains/loses focus
load Resource finished loading
DOMContentLoaded HTML parsed, DOM ready
scroll Element scrolled
resize Window resized

Keyboard Events

  document.addEventListener('keydown', (e) => {
    console.log(e.key);      // 'Enter', 'a', 'Shift'
    console.log(e.code);     // physical key: 'KeyA', 'Enter'
    console.log(e.ctrlKey);  // modifier keys
    console.log(e.shiftKey);
    console.log(e.altKey);
    console.log(e.metaKey); // Cmd on Mac

    if (e.key === 'Escape') closeModal();
    if (e.key === 'Enter' && e.ctrlKey) submitForm();
});
  

Use e.key for character input; e.code for game controls or shortcuts independent of layout.

Form Events

  <form id="myForm">
    <input id="email" type="email" required>
    <button type="submit">Submit</button>
</form>

<script>
document.querySelector('#myForm').addEventListener('submit', (e) => {
    e.preventDefault();
    const email = document.querySelector('#email').value;
    console.log('Submitted:', email);
});

document.querySelector('#email').addEventListener('input', (e) => {
    console.log('Typing:', e.target.value);
});
</script>
  

Event Propagation: Capturing and Bubbling

Events travel in three phases:

  1. Capturing — from window down to target
  2. Target — event reaches target element
  3. Bubbling — from target back up to window
  document.querySelector('#parent').addEventListener('click', () => {
    console.log('Parent — bubble phase');
});

document.querySelector('#child').addEventListener('click', (e) => {
    console.log('Child — bubble phase');
    // e.stopPropagation(); // prevents parent handler
});

// Capturing phase (third argument true, or { capture: true })
document.querySelector('#parent').addEventListener('click', () => {
    console.log('Parent — capture phase');
}, { capture: true });
  

Default: listeners run in bubbling phase.

Event Delegation

Attach one listener to a parent for dynamic children:

  document.querySelector('#list').addEventListener('click', (e) => {
    if (e.target.matches('li')) {
        e.target.classList.toggle('done');
    }
    if (e.target.matches('.delete-btn')) {
        e.target.closest('li').remove();
    }
});
  

Benefits: works for future elements, fewer listeners, better memory use.

Listener Options

  button.addEventListener('click', handler, {
    once: true,      // auto-remove after first invocation
    passive: true,   // won't call preventDefault — scroll performance
    capture: false,  // bubble phase (default)
    signal: controller.signal  // AbortController for cleanup
});

const controller = new AbortController();
element.addEventListener('click', handler, { signal: controller.signal });
controller.abort(); // removes all listeners with this signal
  

Use { passive: true } on touch and scroll listeners unless you need preventDefault().

Removing Listeners

  function handleClick() {
    console.log('Clicked');
}

button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);
// Must pass same function reference — anonymous functions can't be removed
  

Custom Events

  const event = new CustomEvent('userLoggedIn', {
    detail: { username: 'alice' },
    bubbles: true
});
document.dispatchEvent(event);

document.addEventListener('userLoggedIn', (e) => {
    console.log(e.detail.username);
});
  

Complete Example

  <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Events Example</title>
</head>
<body>
    <button id="btn">Click me</button>
    <p id="output">Waiting...</p>

    <script>
        const btn = document.querySelector('#btn');
        const output = document.querySelector('#output');
        let count = 0;

        btn.addEventListener('click', () => {
            count++;
            output.textContent = `Clicked ${count} time(s)`;
        });

        document.addEventListener('keydown', (e) => {
            if (e.key === 'r' && !e.ctrlKey) {
                count = 0;
                output.textContent = 'Reset!';
            }
        });
    </script>
</body>
</html>
  

Best Practices

  1. Use addEventListener — not inline HTML handlers
  2. Delegate for lists and dynamic content
  3. Debounce input handlers for search (see Advanced Functions)
  4. Passive scroll listeners for performance
  5. Clean up listeners when components unmount (SPA frameworks)

Troubleshooting

Handler fires twice

  • Listener registered multiple times; check for duplicate addEventListener
  • Event bubbling — use stopPropagation if intentional single handler

preventDefault not working

  • Listener may be passive — remove passive: true
  • Event already completed — call during synchronous handler

Keyboard shortcut conflicts with browser

  • Use e.preventDefault() in handler; check e.target isn’t an input

Events connect user actions to code — understand propagation, delegation, and the event object for robust interactive applications.