Introduction to browser events

An event is the browser’s way of telling your code that something happened. A click landed, a key went down, an image finished loading, a form got submitted. Every DOM node emits these signals, and your job as a developer is to listen for the ones you care about and run code in response.

Events aren’t unique to the DOM. Timers, network requests, and Node.js streams all use the same emit-and-listen pattern. But the DOM is where you’ll meet events first, so that’s where we start.

Here’s a sampler of the events you’ll reach for most often. Skim it now; you don’t need to memorize anything.

Mouse events

  • click — the mouse clicks an element (a tap on a touchscreen fires it too).
  • contextmenu — a right-click on an element.
  • mouseover / mouseout — the cursor moves onto / off an element.
  • mousedown / mouseup — a mouse button is pressed / released over an element.
  • mousemove — the mouse moves.

Keyboard events

  • keydown and keyup — a key is pressed and released.

Form element events

  • submit — the visitor submits a <form>.
  • focus — the visitor focuses an element, e.g. an <input>.

Document events

  • DOMContentLoaded — the HTML is parsed and the DOM tree is fully built.

CSS events

  • transitionend — a CSS transition finishes.

That’s a small slice. There are hundreds more, and we’ll dig into the important families (mouse, keyboard, form, scroll) in later chapters. For now the mechanics of attaching code to any of them is what matters.

user action
click 🖱️
browser
builds event object
your code
handler(event)
An event is a signal from the browser to your handler function.

Event handlers

To react to an event, you assign a handler — a function that runs when the event fires. Handlers are the bridge between a user doing something and your JavaScript running.

There are three ways to attach one. They range from quick-and-dirty to flexible-and-verbose. We’ll walk through all three, starting with the simplest.

Way 1: the HTML attribute

You can set a handler right in the markup with an attribute named on<event>. For a click, that attribute is onclick.

<input value="Click me" onclick="alert('Click!')" type="button">

Click the button and the code inside onclick runs.

Notice the quotes: the attribute value uses double quotes, so the JavaScript string inside uses single quotes. Mix them up — onclick="alert("Click!")" — and the browser sees the attribute value end at the second double quote, leaving Click!") as stray markup. The handler breaks.

An HTML attribute is a cramped place for real logic. Once you have more than a line or two, define a function and call it:

<script>
  function countSteps() {
    for (let i = 1; i <= 3; i++) {
      alert("Step number " + i);
    }
  }
</script>

<input type="button" onclick="countSteps()" value="Count steps!">

One detail about HTML: attribute names are case-insensitive. ONCLICK, onClick, and onCLICK all work. Convention is lowercase — onclick — so stick with that.

Way 2: the DOM property

Every element exposes an on<event> property in JavaScript. Assign a function to it and you have a handler.

<input id="elem" type="button" value="Click me">
<script>
  elem.onclick = function() {
    alert('Thank you');
  };
</script>

Here’s the thing that surprises people: the HTML attribute and the DOM property are the same slot. When you write onclick="..." in markup, the browser reads that attribute, wraps its contents in a function, and stores that function in elem.onclick. The attribute is just a way to initialize the property before your script runs.

So these two snippets end up in the exact same state:

<!-- initialized from the HTML attribute -->
<input type="button" onclick="alert('Click!')" value="Button">
<!-- initialized from a script -->
<input type="button" id="button" value="Button">
<script>
  button.onclick = function() {
    alert('Click!');
  };
</script>

The only difference is who set button.onclick: the parser, or your code.

onclick=“…” (HTML)
elem.onclick = fn (JS)
elem.onclick  ← one function, max
The HTML attribute and the DOM property write to one shared slot.

Because there’s only one onclick property, you can hold only one handler this way. A second assignment overwrites the first:

<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
  elem.onclick = function() { // overwrites the handler from the attribute
    alert('After'); // only this one runs
  };
</script>

To detach a handler set this way, assign null:

elem.onclick = null;

Accessing the element: this

Inside a handler, this refers to the element the handler is attached to. That gives you a handle on the element without needing an id or a lookup.

<button onclick="alert(this.innerHTML)">Click me</button>

Click it and you get Click me — the button reading its own content through this. Try it live below. Each button runs the same handler, yet this resolves to whichever one you actually pressed:

interactiveOne handler, many elements — this points to the clicked one

Common mistakes

A few subtleties trip up nearly everyone starting out.

Assign the function, don’t call it. You can hand an existing function to a handler property:

function sayThanks() {
  alert('Thanks!');
}

elem.onclick = sayThanks;

Assign sayThanks, not sayThanks(). The parentheses change everything:

// right — store the function itself
button.onclick = sayThanks;

// wrong — store what the function returns
button.onclick = sayThanks();

With parentheses, sayThanks() runs immediately, right when that line executes, and its return value (here undefined, since it returns nothing) gets stored in onclick. Now clicking does nothing, and your “Thanks!” alert fired once at page load instead.

onclick = sayThanks  → onclick holds the function; runs on each click

onclick = sayThanks()  → onclick holds undefined; nothing runs on click

Reference vs. call: the parentheses decide.

But in markup, you do need the parentheses:

<input type="button" id="button" onclick="sayThanks()">

Why the flip? Because the attribute holds code, not a reference. When the browser parses onclick="sayThanks()", it generates a wrapper function whose body is that code:

button.onclick = function() {
  sayThanks(); // <-- the attribute's text becomes this line
};

So the call happens inside the generated wrapper, once per click. Different context, different rule.

Don’t use setAttribute for handlers. Attributes are always strings, so a function passed to setAttribute gets stringified and never runs as code:

// clicking <body> throws, because the function was coerced to a string
document.body.setAttribute('onclick', function() { alert(1) });

DOM property names are case-sensitive. Write elem.onclick, not elem.ONCLICK. HTML attributes forgive casing; JavaScript properties do not.

Way 3: addEventListener

The DOM property has a hard ceiling: one handler per event. That’s a real problem in practice. Imagine one module wants to highlight a button on click, and a separate module wants to log the click. Both write to onclick, and the second wins:

input.onclick = function() { alert(1); };
// ...somewhere else in the codebase
input.onclick = function() { alert(2); }; // silently clobbers the first

Standards authors saw this coming and added a pair of methods free of that limitation: addEventListener and removeEventListener.

element.addEventListener(event, handler, [options]);
event
The event name, e.g. “click”. Note: a plain string, no on prefix.
handler
The function to run.
options

An optional object:

  • once — if true, the listener removes itself automatically after firing one time.
  • capture — which phase to handle the event in, covered in Bubbling and capturing. For historical reasons you can pass a bare false/true here instead of an object; it means {capture: false/true}.
  • passive — if true, you’re promising the handler won’t call preventDefault(), which lets the browser optimize scrolling. More on that in Browser default actions.

Removing works the same shape:

element.removeEventListener(event, handler, [options]);

The payoff: call addEventListener as many times as you like and every handler runs.

<input id="elem" type="button" value="Click me"/>

<script>
  function handler1() {
    alert('Thanks!');
  }

  function handler2() {
    alert('Thanks again!');
  }

  elem.onclick = () => alert("Hello");
  elem.addEventListener("click", handler1); // Thanks!
  elem.addEventListener("click", handler2); // Thanks again!
</script>

Click once and all three fire — the onclick property and the two listeners coexist. They’re independent registration channels. In real code you’d usually pick one style and stick with it, but it’s worth knowing they don’t conflict.

See it for yourself. The demo below writes to the page instead of alerting, so you can watch every handler on the pile respond to a single click:

interactiveonclick vs. a stack of listeners
elem.onclick
fnA ↩ replaced by fnB
only fnB survives
addEventListener(“click”, …)
fnA
fnB
fnC
all three run
onclick holds one handler; addEventListener keeps a growing list.

The event object

Reacting to “a click happened” is often not enough. You want the details: where was the pointer, which key went down, which element got hit. The browser packages all of that into an event object and passes it to your handler as the first argument.

<input type="button" value="Click me" id="elem">

<script>
  elem.onclick = function(event) {
    // event type, the handling element, and click coordinates
    alert(event.type + " at " + event.currentTarget);
    alert("Coordinates: " + event.clientX + ":" + event.clientY);
  };
</script>

A few properties you’ll use constantly:

event.type
The event name — here, “click”.
event.currentTarget
The element whose handler is running. Usually identical to this — but if the handler is an arrow function, or its this was bound elsewhere, currentTarget still gives you the element reliably.
event.clientX / event.clientY
The cursor’s coordinates relative to the window, for pointer events.

Click anywhere in the pad below to see a live event object. Every click hands your handler a fresh one, filled in with the exact type and coordinates of what just happened:

interactiveReading the event object on each click

There are many more, and most are specific to the event family: keyboard events carry key and code, pointer events carry coordinates, and so on. We’ll meet those as we cover each type.

Object handlers: handleEvent

addEventListener accepts more than plain functions. Hand it an object with a handleEvent method and the browser calls that method when the event fires.

<button id="elem">Click me</button>

<script>
  let obj = {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    }
  };

  elem.addEventListener('click', obj);
</script>

When the listener is an object, the browser invokes obj.handleEvent(event) for you. This is handy when your handler needs state — the object can carry data alongside its behavior.

That naturally extends to class instances:

<button id="elem">Click me</button>

<script>
  class Latch {
    handleEvent(event) {
      switch (event.type) {
        case 'mousedown':
          elem.innerHTML = "Latch held down";
          break;
        case 'mouseup':
          elem.innerHTML += "...and released.";
          break;
      }
    }
  }

  let latch = new Latch();

  elem.addEventListener('mousedown', latch);
  elem.addEventListener('mouseup', latch);
</script>

One latch object handles both events. You still register each event type explicitly — latch receives only mousedown and mouseup because those are the two you subscribed it to, nothing else.

handleEvent doesn’t have to do all the work inline. It can dispatch to per-event methods, which keeps each piece of logic in its own tidy place:

<button id="elem">Click me</button>

<script>
  class Latch {
    handleEvent(event) {
      // build a method name: mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      elem.innerHTML = "Latch held down";
    }

    onMouseup() {
      elem.innerHTML += "...and released.";
    }
  }

  let latch = new Latch();
  elem.addEventListener('mousedown', latch);
  elem.addEventListener('mouseup', latch);
</script>
mousedown
handleEvent()
onMousedown()
onMouseup()
handleEvent routes each event to its own method.

Here that routing object is wired up for real. One Latch instance is subscribed to both mousedown and mouseup; handleEvent builds a method name from event.type and dispatches. Press and hold the button, then release:

interactiveAn object as the listener, routing to per-event methods

Summary

Three ways to attach an event handler:

  1. HTML attributeonclick="...".
  2. DOM propertyelem.onclick = function.
  3. Methodselem.addEventListener(event, handler[, options]) to add, removeEventListener to remove.

HTML attributes see little use: JavaScript wedged inside a tag reads awkwardly, and there’s no room for real logic. They’re fine for a one-liner demo.

DOM properties are perfectly usable, with one catch — a single handler per event. Often that limit never bites you, so this style is common and clean.

addEventListener is the most capable and the most verbose. A few events (DOMContentLoaded, transitionend, and others) work only through it. It also accepts an object with a handleEvent method, which the browser calls when the event occurs.

However you attach it, the handler always receives an event object as its first argument, carrying the details of what happened — the type, the target, the coordinates, and more.

Later chapters go deeper: how events travel through the DOM tree, how to stop the browser’s default reactions, and the specifics of mouse, keyboard, and form events.