LEARN TO CODE | JAVASCRIPT | 03 INTERACTIVE PAGES
03.2 Events and event listeners
Register callbacks that respond to page events and keep program state and visible output in step.
01 | A PAGE SHOULD RESPOND TO THE VISITOR
A script that runs on page load can build a list. A button that adds to a score needs a different trigger: the visitor decides when to activate it.
An event is a signal that something happened. A click, a change to a field and a form submission are examples. An event listener registers a function to run when a matching event is dispatched.
The function is a callback: you pass it to the browser now, and the browser invokes it when needed. This uses the same function-as-a-value idea introduced with array callbacks.
In VS Code, create index.html, script.js and, when shown, style.css in one practice folder. Save changes, open the HTML page in your browser and inspect both the page and its Console. Replace the previous example unless a task explicitly extends it.
02 | A FIRST LISTENER
<button id="hello-button" type="button">Say hello</button>
<p id="hello-output" role="status" aria-live="polite"></p>
const button = document.querySelector("#hello-button");
const output = document.querySelector("#hello-output");
function sayHello() {
output.textContent = "Hello!";
}
button.addEventListener("click", sayHello);
This fragment requires the shown markup and a deferred external script. addEventListener receives the event type as a string and the callback function as its second argument.
Use sayHello, without parentheses. sayHello() would run immediately and pass its returned value, rather than register the function to be called on activation.
type="button" makes this a plain action button. Inside a form, a button without an explicit type normally acts as a submit button; use the type that matches the job.
03 | KEEP STATE SEPARATE FROM OUTPUT
let count = 0;
const output = document.querySelector("#event-count");
function renderCount() {
output.textContent = `Clicks: ${count}`;
}
function addOne() {
count += 1;
renderCount();
}
This fragment is part of the complete counter below. count is the program state. renderCount displays it. addOne changes the state and then requests an updated display.
Changing count alone does not automatically alter a paragraph. Conversely, changing a paragraph’s text does not automatically update the count variable. Keeping an explicit rendering step helps avoid those two getting out of sync.
Each listener call sees the updated count from the surrounding scope. It is not reset to zero on every click because the initialisation is outside the callback.
TRY IT YOURSELF
What would happen if let count = 0 were inside addOne?
Reveal guidance
Each invocation would start again from zero and increase to one. The output would repeatedly show 1 instead of accumulating clicks.
04 | THE EVENT OBJECT GIVES CONTEXT
const input = document.querySelector("#event-name");
input.addEventListener("input", (event) => {
console.log(event.currentTarget.value);
});
The browser supplies an event object to the callback. event is our parameter name, not a reserved word. currentTarget is the object whose listener is currently running, so its value gives the current input text here.
event.target identifies where the event originated. With nested elements and bubbling, target and currentTarget can differ. An event can bubble through ancestors, allowing an ancestor listener to receive it too.
You do not need to stop bubbling for this example. Keep listeners on the control they serve until you have a clear reason to use delegation.
05 | INPUT IS DIFFERENT FROM CHANGE
The input event fires as the visitor changes the field value, making it useful for live feedback. For a text field, change commonly fires when a changed value is committed, such as when the field loses focus.
Use input for the live greeting below. Do not use keydown alone to detect every change: pasting and other input methods can change text without an ordinary typed key.
Setting input.value in your own script does not automatically dispatch an input event. If a programmatic change needs updated output, call your rendering function deliberately.
06 | TOGGLE CONTENT AND EXPLAIN THE STATE
A show-help button should update both the content’s visibility and the button’s description of its state. hidden controls the panel, while aria-expanded tells assistive technologies whether it is open.
help.hidden = !help.hidden;
helpButton.setAttribute("aria-expanded", String(!help.hidden));
helpButton.textContent = help.hidden ? "Show help" : "Hide help";
This fragment uses the elements selected in the complete example. ! reverses the boolean hidden state. setAttribute supplies text for the ARIA attribute, producing "true" or "false".
aria-controls connects the button to the panel’s ID. It does not toggle the panel by itself: the JavaScript performs that behaviour.
07 | COMPLETE COUNTER, GREETING AND HELP
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A click counter and live greeting</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<main>
<h1>A click counter and live greeting</h1>
<p id="event-count" role="status" aria-live="polite">Clicks: 0</p>
<button id="event-add" type="button">Add one</button>
<button id="event-reset" type="button">Reset count</button>
<p><label for="event-name">Practice name : </label> <input id="event-name" type="text" maxlength="40" autocomplete="off"></p>
<p id="event-greeting" role="status" aria-live="polite">Type a practice name.</p>
<button id="event-help-button" type="button" aria-expanded="false" aria-controls="event-help">Show help</button>
<p id="event-help" hidden>Use Tab to move between controls. Enter or Space activates a focused button.</p>
</main>
</body>
</html>
script.js
let count = 0;
const countOutput = document.querySelector("#event-count");
const addButton = document.querySelector("#event-add");
const resetButton = document.querySelector("#event-reset");
const nameInput = document.querySelector("#event-name");
const greeting = document.querySelector("#event-greeting");
const helpButton = document.querySelector("#event-help-button");
const help = document.querySelector("#event-help");
function renderCount() {
countOutput.textContent = `Clicks: ${count}`;
}
function addOne() {
count += 1;
renderCount();
}
addButton.addEventListener("click", addOne);
resetButton.addEventListener("click", () => {
count = 0;
renderCount();
});
nameInput.addEventListener("input", (event) => {
const name = event.currentTarget.value.trim();
greeting.textContent = name === "" ? "Type a practice name." : `Hello, ${name}!`;
});
helpButton.addEventListener("click", () => {
help.hidden = !help.hidden;
helpButton.setAttribute("aria-expanded", String(!help.hidden));
helpButton.textContent = help.hidden ? "Show help" : "Hide help";
});
renderCount();
08 | TRY THE CONTROLS
Clicks: 0
Type a practice name.
Use Tab to move between controls. Enter or Space activates a focused button.
Use Add one several times, reset, type a fictional name and toggle help. The controls run independently but use the same pattern: select once, register a callback, update state or output.
TRY IT YOURSELF
Predict the count after Add, Add, Reset, Add. Then verify it.
Reveal guidance
The sequence is 1, 2, 0, 1. Reset must change count itself, not just overwrite the output paragraph.
09 | MAKE IT WORK WITHOUT A MOUSE
Use actual button elements for actions rather than a clickable span or div. A native button already participates in keyboard focus and supports expected activation behaviour.
Press Tab through the controls and activate a focused button using Enter or Space. Keep visible focus styling. A live output region can announce an update without forcing the visitor’s focus away from the control.
Every text input needs a visible, associated label. A placeholder alone is not a substitute. Notice the space and colon between each label and its control.
10 | ADD LISTENERS ONCE
Register listeners during setup rather than every time you render the count. Repeatedly adding fresh anonymous listeners can make one activation trigger the same action several times.
function handleClick() {
console.log("Activated");
}
button.addEventListener("click", handleClick);
// Later, if this behaviour is no longer needed:
button.removeEventListener("click", handleClick);
This fragment assumes button has been selected. Removing a listener requires the matching function reference and matching capture option. A newly written anonymous function is not the same function object as the earlier one.
For this small page, keeping listeners active for the page lifetime is fine. Do not add complexity without a reason.
11 | EXTEND THE INTERACTION
TRY IT YOURSELF
Add a Subtract one button, but keep the count at zero or above. Test it at zero and after two additions.
Reveal guidance
Use a new type=button control and one click listener. If count > 0, subtract one; then call renderCount. At zero, the count stays zero. Do not read the paragraph text to reconstruct the state.
Reloading this page resets ordinary in-memory state. Later in this section, browser storage will preserve a deliberately chosen preference.
KEEP LEARNING
Previous page 03.3 Forms and input validation
Download Section 03 example programs
Further reading: The DOM · Event listeners · Form validation · Browser storage