Skip to content
JavaScript js events 5 min read

Events Explained

JavaScript in the browser is fundamentally event-driven: instead of running top to bottom and stopping, your code waits for things to happen — a click, a keystroke, a finished network request — and reacts. An event is a signal that something occurred, and an event handler is the function you register to run when it does. Mastering this model is the difference between a static page and a living, interactive application.

The event-driven model

When a page loads, the browser builds the DOM and then enters a loop, watching for activity. Each user action or browser milestone produces an Event object that is dispatched to a target element. If a handler is registered for that event type on that target, the browser calls it — usually asynchronously, after the current code finishes.

This loop is why JavaScript feels responsive without you writing any polling code. You declare what should happen when, and the runtime takes care of the timing.

user clicks button


browser creates a "click" Event


event dispatched to the <button>


your registered handler runs

The event loop is single-threaded. A handler that does heavy synchronous work blocks every other event — including rendering — until it returns. Keep handlers fast and offload heavy work.

Common event types

There are dozens of event types, grouped by what triggers them. These are the ones you will use most often.

EventFires whenTypical target
clickAn element is clicked (or activated via keyboard)buttons, links, any element
inputAn input’s value changes, on every keystroke<input>, <textarea>, <select>
changeA control’s value is committed (often on blur)form controls
keydown / keyupA key is pressed / releaseddocument, focused element
submitA form is submitted<form>
loadA resource (page, image, script) finishes loadingwindow, <img>
DOMContentLoadedThe HTML is parsed and the DOM is readydocument
mouseenter / mouseleaveThe pointer enters / leaves an elementany element
scrollAn element or the page is scrolledelement, window

Three ways to attach a handler

There are three mechanisms for wiring up handlers. They are not equal — only one is recommended for real code.

1. Inline HTML attributes (avoid)

You can place JavaScript directly in markup with an on* attribute. This mixes behavior into your HTML, runs into Content-Security-Policy issues, and only allows one handler.

<button onclick="alert('clicked!')">Don't do this</button>

2. DOM on-property (limited)

Every element exposes onclick, oninput, and similar properties. Assigning a function works, but assigning a second one overwrites the first — you can only ever have one handler per event per element.

const btn = document.querySelector("#save");
btn.onclick = () => console.log("saved");
// This replaces the handler above — the first is lost:
btn.onclick = () => console.log("also saved");

addEventListener(type, handler, options) is the modern standard. It supports multiple handlers on the same element, fine-grained options (such as once, passive, and capture), and clean removal with removeEventListener.

const btn = document.querySelector("#save");

function onSave() {
  console.log("saved");
}

btn.addEventListener("click", onSave);
btn.addEventListener("click", () => console.log("logging too"));

// Both run on click. To detach the first later:
btn.removeEventListener("click", onSave);

To remove a listener you must pass the same function reference you added. Anonymous arrow functions can’t be removed this way — keep a named reference if you need to detach.

Here is a self-contained, runnable demo wiring up a click handler that updates the page:

<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: system-ui, sans-serif; padding: 24px; }
    button { font-size: 1rem; padding: 8px 16px; cursor: pointer; }
    #count { font-weight: bold; font-size: 1.5rem; }
  </style>
</head>
<body>
  <button id="inc">Click me</button>
  <p>Clicks: <span id="count">0</span></p>

  <script>
    const button = document.querySelector("#inc");
    const output = document.querySelector("#count");
    let clicks = 0;

    button.addEventListener("click", () => {
      clicks += 1;
      output.textContent = clicks;
    });
  </script>
</body>
</html>

Reacting to different event types

A single page often listens for several event types at once. This demo responds to typing (input) and key presses (keydown) on the same field:

<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: system-ui, sans-serif; padding: 24px; }
    input { font-size: 1rem; padding: 6px; width: 240px; }
    p { margin: 8px 0; }
  </style>
</head>
<body>
  <input id="field" placeholder="Type something..." />
  <p>Live value: <strong id="live"></strong></p>
  <p>Last key: <strong id="key"></strong></p>

  <script>
    const field = document.querySelector("#field");
    const live = document.querySelector("#live");
    const key = document.querySelector("#key");

    field.addEventListener("input", (event) => {
      live.textContent = event.target.value;
    });

    field.addEventListener("keydown", (event) => {
      key.textContent = event.key;
    });
  </script>
</body>
</html>

Every handler receives the Event object as its first argument. Above, event.target is the element that fired the event and event.key is the pressed key. The next page in this section covers that object in depth.

Waiting for the DOM

If your script runs in <head> before the body is parsed, querySelector returns null because the elements don’t exist yet. Wrap startup logic in DOMContentLoaded, or place your <script> at the end of the body (or add defer to the tag).

document.addEventListener("DOMContentLoaded", () => {
  const app = document.querySelector("#app");
  console.log("DOM ready:", app !== null);
});

Output:

DOM ready: true

Best Practices

  • Prefer addEventListener over inline attributes and on* properties — it scales, supports options, and keeps behavior out of markup.
  • Keep a named reference to any handler you intend to remove with removeEventListener.
  • Keep handlers small and fast; the single-threaded event loop means slow handlers freeze the UI.
  • Use the { once: true } option for one-shot handlers instead of manually removing them.
  • Add { passive: true } to high-frequency listeners like scroll and touchmove to let the browser optimize scrolling.
  • Throttle or debounce noisy events (scroll, resize, input) so handlers don’t fire hundreds of times per second.
  • Read event.target to identify the source element rather than relying on closed-over variables.
Last updated June 1, 2026
Was this helpful?