← All lessons

/ JAVASCRIPT FUNDAMENTALS /

Understanding JavaScript Event Listeners

Making a web page react to the user.

A web page made with HTML and CSS can display information, but JavaScript is what lets that page respond to what a user does.

A user might click a button, type into an input, submit a form, press a key, or move through a page. JavaScript calls these actions events.

An event listener tells JavaScript:

"When this particular thing happens, run this code."

That simple idea powers a huge amount of what we interact with on the web. Menus, forms, modals, search boxes, shopping carts, dashboards, and countless other interfaces depend on events.

Start with the problem

Imagine we have a button:

HTML

<button id="helloButton">Say Hello</button>

The button exists, but clicking it does nothing. HTML created the element. Now JavaScript needs to define what happens when someone interacts with it.

JavaScript

const button = document.querySelector("#helloButton");
button.addEventListener("click", function () {
  console.log("Hello!");
});

Button

What are we listening to?

"click"

What are we listening for?

Function

What should happen?

ELEMENT → EVENT → RESPONSE

Let's make something happen on the page

HTML

<button id="helloButton">Say Hello</button>
<p id="message"></p>

JavaScript

const button = document.querySelector("#helloButton");
const message = document.querySelector("#message");

button.addEventListener("click", function () {
  message.textContent = "Hello!";
});

Live example

Waiting for a click...

Go ahead — click it. That's a live version of the code above, actually running.

User clicks button

JavaScript detects event

Function runs

Interface changes

That's the beginning of event-driven programming: code that waits, then reacts.

Separate the "what" from the "when"

const button = document.querySelector("#helloButton");
const message = document.querySelector("#message");

function sayHello() {
  message.textContent = "Hello!";
}

button.addEventListener("click", sayHello);

The function defines what should happen. The event listener defines when it should happen.

What

function sayHello() { ... }

When

button.addEventListener("click", sayHello);

Splitting these apart doesn't matter much for one button. It matters a lot once an application has dozens of them.

What actually happened?

button.addEventListener("click", function (event) {
  console.log(event);
});
button.addEventListener("click", function (event) {
  console.log(event.target);
});

JavaScript gives us information about the interaction through the event object. event.target tells us which element actually triggered it.

Click one of the three buttons below.

Live example

Click one of the buttons above.

Let's build something

HTML

<button id="counterButton">Clicked 0 times</button>

JavaScript

const button = document.querySelector("#counterButton");

let count = 0;

button.addEventListener("click", function () {
  count++;
  button.textContent = `Clicked ${count} times`;
});

Live example

State

let count = 0;

Event

click

Update state

count++;

Update interface

button.textContent = ...

This pattern appears everywhere in front-end development:

Something happens → data changes → interface updates.

React and other frameworks make this process more structured, but understanding the underlying idea first makes those abstractions easier to understand.

/ DEBUGGING /

Breaking the code is part of learning it

HTML

<button id="helloButton">Say Hello</button>

Broken JavaScript

const button = document.querySelector("#heloButton");

button.addEventListener("click", function () {
  console.log("Hello!");
});

In the JavaScript

#heloButton

The actual id, in the HTML

#helloButton

Why doesn't our event listener work?

console.log(button);

Result

null

The problem isn't actually the event listener. The problem happened before the listener was ever attached — JavaScript couldn't find the element in the first place.

What did I expect?

What actually happened?

Where do those two things become different?

Test one assumption at a time.

Passing a function vs. calling a function

function sayHello() {
  console.log("Hello!");
}

Incorrect

button.addEventListener("click", sayHello());

Correct

button.addEventListener("click", sayHello);

sayHello() runs the function immediately. sayHello passes the function itself to the event listener, so JavaScript can run it later, when the click actually happens.

sayHello()

Run it now

sayHello

Use this function later

Clicks are only the beginning

clickinputchangesubmitkeydownkeyupmouseentermouseleavefocusblur

HTML

<input id="nameInput" type="text" placeholder="Type your name">
<p id="preview"></p>

JavaScript

const input = document.querySelector("#nameInput");
const preview = document.querySelector("#preview");

input.addEventListener("input", function () {
  preview.textContent = input.value;
});

Live example

Whatever you type shows up here, live.

The event changed from "click" to "input", but the mental model stayed exactly the same:

ELEMENT → EVENT → RESPONSE

Sometimes we don't want the browser's default behavior

HTML

<form id="signupForm">
  <input id="email" type="email">
  <button type="submit">Sign Up</button>
</form>

JavaScript

const form = document.querySelector("#signupForm");

form.addEventListener("submit", function (event) {
  event.preventDefault();

  console.log("Form submitted");
});

Browsers already know how forms normally behave — submit one, and the page reloads. event.preventDefault() tells the browser: "Don't do the normal thing yet. I'm going to handle this with JavaScript."

That's what makes validation, sending data to an API, and updating the interface in place possible.

/ NEXT LEVEL /

One listener can sometimes handle many elements

HTML

<ul id="todoList">
  <li>
    Learn JavaScript
    <button class="deleteButton">Delete</button>
  </li>
  <li>
    Learn React
    <button class="deleteButton">Delete</button>
  </li>
  <li>
    Build a project
    <button class="deleteButton">Delete</button>
  </li>
</ul>

JavaScript

const list = document.querySelector("#todoList");

list.addEventListener("click", function (event) {
  if (event.target.matches(".deleteButton")) {
    event.target.closest("li").remove();
  }
});

A click on a button inside the list "bubbles up" to the list itself, and the listener there can check event.target to figure out which button actually got clicked. Instead of creating a listener for every child element, we listen once, at the parent, and let the event tell us which child caused it.

Try it — delete a few items.

Live example

  • Learn JavaScript
  • Learn React
  • Build a project

The bigger idea behind event listeners

Something happens.

Our application detects it.

Our code decides how to respond.

Dropdown

click → open menu

Search

input → update results

Form

submit → validate → send data

Keyboard shortcut

keydown → perform action

Modal

click → change interface state

Frameworks like React express these same ideas a little differently:

<button onClick={handleClick}>
  Say Hello
</button>

The syntax changes, but the underlying event-driven mental model is still there. Understanding the browser first makes the abstraction easier to understand later.

/ CHALLENGE /

Your turn

Starting HTML

<button id="button">Click me</button>
<p id="message">You haven't clicked yet.</p>

Make the text update every time the button is clicked:

Clicked 1 time → Clicked 2 times → Clicked 3 times → ...

Extension: add a Reset button that returns the counter to zero.

Before you look at hints, think through:

  • What information needs to change?
  • What event are we listening for?
  • What element needs to update?

Try your solution here

You haven't clicked yet.

How I teach

Make the why clear. Learn by building. Build confidence through debugging.

I want students to understand more than the code that solves one problem. I want them to develop a process they can use when they encounter the next one.