Events: reacting to clicks, typing and keys
Why:A page comes alive when it reacts to the visitor. Afterwards you’ll attach handlers to clicks, typing and keys, understand how an event bubbles up the tree and why delegation helps, and not break control from the keyboard.
25 minutes
Read by speech synthesis (Qwen3-TTS); the voice is not a person’s.
An event is a message from the browser about something a visitor did: a click, typing, a key press. A script subscribes to it with addEventListener and reacts.
A page that listens
A browser constantly notices what a visitor does: pressed, swiped, typed a letter, switched tabs, turned the phone. The browser wraps each observation in an event and tells the page. The page does nothing until it is “subscribed” to an event, and a script has to do the subscribing.
const button = document.querySelector('#menu-button');
button.addEventListener('click', (event) => {
console.log('Clicked, the type of event:', event.type);
}); The addEventListener method takes two arguments: the type of event and a handler, the function the browser calls when the event happens. It passes the function an event object. The object has type (which event), target (where it happened), key for the keyboard (which key), and much else. A subscription can be removed with removeEventListener.
There are two older ways, and both are worse. An attribute in the markup (<button onclick="...">) mixes markup with code and allows only one handler. A property (button.onclick = ...) also gives one handler per event: the next one overwrites the last. addEventListener lets you subscribe as many functions to one event as you like.
The events a site needs
There are dozens of events, but almost everything on a site is built on five:
click: a press with a mouse or a finger, and on a button also the Enter and Space keys;input: the value of a field changes while the person types (whilechangefires when they have finished, for example left the field or switched a checkbox);submit: a form is sent, by the button or by Enter;keydown: a key is pressed, andevent.keynames it ('Escape','Enter','a');DOMContentLoaded: the page is parsed. In our editor module scripts run after that moment anyway, so you don’t need to listen for it.
Canceling the browser’s action
Many events have a default action: a link follows its address, a form is sent and the page reloads, Tab moves the focus. When you need to do it your own way, you cancel the action by calling event.preventDefault().
form.addEventListener('submit', (event) => {
event.preventDefault(); // the page won't reload
// then we send the data ourselves: that is the lesson on fetch
}); Without that line, Enter in the search field sends the form and the page reloads with an empty result: the most common beginner mistake.
Check yourself: what happens if you forget preventDefault in a submit handler?
The browser does the default action: it sends the form and reloads the page, and everything the script managed to do is gone.
Bubbling and delegation
An event doesn’t stay in one place. When you click a link inside a card, the browser first gives the event to the link itself, and then it bubbles up the ancestors: the card, the container of the list, body, document. Each of them may have its own handler and receive the same event. That is why the event object has two addresses:
event.target: where the event happened (the deepest element that was clicked);event.currentTarget: the element the running handler is attached to.
This gives a trick that everyone uses: delegation. Instead of a handler on each of a hundred cards, you attach one to the container and look at event.target to see what was actually clicked:
cards.addEventListener('click', (event) => {
const card = event.target.closest('.card'); // the nearest card, going up from the click
if (!card) return; // the click missed every card
card.classList.toggle('selected');
}); The closest method looks for a matching element from the place of the click upward through the tree. There are two gains. There are fewer listeners, and above all the handler also works for cards that a script adds later: their events bubble up to the container too. So if you have a list that changes, attach the handler to the parent.
Bubbling can be stopped with event.stopPropagation(), but it is rarely worth doing: it breaks other people’s handlers that you may not have thought of.
See how an event travels the tree. Choose where to click, which nodes have handlers, and try to stop the bubbling:
a: the event passes through, there is no handlerarticle.card: the event passes through, there is no handlerdiv.cards: the handler ran: target = a, currentTarget = div.cardsbody: the event passes through, there is no handlerdocument: the event passes through, there is no handler
What else happens. The browser will follow the link: nobody canceled the default action.
And the second question of the diagram: how many handlers you need when the cards multiply and when a script has added one:
Click the cards: each answers with its own handler.
Add a card: it will work too, with no new code.
Check yourself: how does event.target differ from event.currentTarget?
target is where the event happened. currentTarget is the element the running handler is attached to. With delegation the handler is on the container (currentTarget), and an element inside it was clicked (target).
Usable from the keyboard
The sneakiest mistake with events is to make something that works only with a mouse. Many people use the keyboard: people with motor impairments, people who listen to a page with a screen reader, and plain people who are faster without a mouse. The rules are short.
- Use a real
button. It gets focus from the Tab key, it is pressed with Enter and Space, a screen reader announces it as “button”, and all of that is free. Adivwith aclickhandler can do none of it, and a link<a href="#">that works as a button confuses both groups. - Report the state. A button that expands a block has an
aria-expandedattribute (falseortrue), and the script changes it together with the block. Thearia-controlsattribute names the id of the controlled block. A screen reader will read “Sections, button, collapsed”. - Close on Escape and return the focus. If the menu was opened with a key, the focus stayed on the button. When you close the menu, don’t lose it:
button.focus(). - Don’t remove the visible focus. The frame around the element that Tab has reached is the only way to know where you are.
The first rule has a second half: if the menu has to work without a script (and we’ll see why at the end of the part), then the list of sections is always visible in the markup, and the script hides it behind the button. Not the other way round.
Practice
We continue with the 2014 page. We’ll add two typical elements: a menu that folds behind a button, and a search through the offers that hides the cards that don’t fit as you type.
Notice three details in the task. The hidden attribute loses to any CSS rule that sets display, so you need the rule [hidden] { display: none !important }. The search form gets a submit handler with preventDefault, or Enter will reload the page. And the cards are searched for at the moment of typing, not once at start: the list may change, and you’ll see that in the lesson on fetch.
As in the earlier lessons, the checks read the markup and the text of the script. Whether it works you check yourself: press Tab until you reach the menu button, open and close it with the keys, and type a word in the search.
What was wrong with the old events
Before addEventListener, handlers were attached in different ways: Internet Explorer up to version 9 used attachEvent, the other browsers used addEventListener, and every developer wrote a wrapper of their own (jQuery, with its .click() and .on(), was among other things such a wrapper). Menus were made of links <a href="#"> with “nothing” where the address should be: they could be clicked with a mouse, weren’t announced as buttons, and added a stray hash sign to the address bar. Bubbling was often stopped out of habit, and so other people’s scripts (analytics, for one) stopped seeing the clicks.
What stayed right: listen for events on a shared parent instead of attaching a hundred handlers, and treat the keyboard as equal to the mouse.
Practice
Preparing your page…
Test
Question 1 of 7
Cards
These cards go into your review once you finish the lesson.
How do you attach an event handler?
Answer
element.addEventListener(type, function). The function gets an event object with type, target, key and other fields. Attributes like onclick in the markup are not used.
What is event bubbling?
Answer
After it fires on an element, the event climbs the ancestors: card, container, body, document. Every ancestor with a handler receives it. event.target stays the element where it all began.
What is delegation, and when is it needed?
Answer
One handler on a shared parent instead of many on the children; inside, it looks at event.target or closest. It is needed when there are many children or a script adds them later.
What is preventDefault for?
Answer
It cancels the browser’s default action: following a link, sending a form. Without it Enter in a form reloads the page, and your code never gets its turn.
How do you make interaction work from the keyboard?
Answer
Use real button and a elements, show the focus, change aria-expanded along with the state, close on Escape and return the focus. A click on a div alone can’t do any of this.
Sources
- Introduction to events (MDN) CC BY-SA, in English
- EventTarget.addEventListener (MDN) CC BY-SA, in English
- Introduction to browser events (The Modern JavaScript Tutorial) link only, in English
- ARIA Authoring Practices Guide: Disclosure (W3C) link only, in English
Videos
Learn JavaScript EventListeners in 4 minutes (Coding2GO)
The video loads from youtube-nocookie.com only when you press play.
Next
You now know: events and bubbling.