What an Event Is
An event is the browser telling you that something happened. A click, a key press, a form submission, an image finishing loading, the window being resized — every one of those is an event, and each is announced the moment it occurs.
Your code does not go looking for events. You register interest once, by attaching a handler — a function the browser will call when that thing happens — and then your function waits. This is the opposite of the flow you have used so far, where each line runs after the one above it. Here, code sits idle until something external triggers it, which is why the style is called event-driven programming.
The waiting costs nothing. Between events the JavaScript thread is completely free, which is how a page stays responsive with hundreds of handlers registered on it. When an event does fire, the browser queues your handler and runs it as soon as the thread is available — the same single thread and the same queue that Lesson 21 explains properly when it reaches the event loop.
Everything interactive on a web page is built from this one idea. A dropdown menu is a click handler that toggles a class. Live validation is an input handler that checks a value. Infinite scroll is a scroll handler that fetches more data. Once you can attach a handler and read the event object, the difference between those three is a matter of detail rather than of technique.
const btn = document.querySelector('#saveBtn');
// Register interest once...
btn.addEventListener('click', function () {
console.log('The button was clicked');
});
// ...and this line runs straight away, without waiting for anything
console.log('Setup finished');
// The handler may now run zero times, once, or fifty times.
// That is entirely up to the user. Three Ways to Attach a Handler
You will meet three ways of connecting a function to an event, and only one of them belongs in real code. Being able to recognise all three matters, because tutorials and older codebases use all three freely.
The first is the HTML attribute — <button onclick="save()">. It is the quickest way to demonstrate an idea, which is exactly why introductory material uses it, and it has real problems. Behaviour becomes mixed into your markup, the function has to be a global so the attribute can find it, only one handler is possible, and the value is a string that no editor or tool can check for mistakes.
The second is the DOM property — btn.onclick = handleClick. This is better: the handler is a real function reference and it lives in your JavaScript. Its limitation is that there is exactly one slot. Assigning a second handler silently replaces the first, so on a page where two separate scripts both care about the same click, one of them quietly loses and nothing reports it.
The third is addEventListener('click', handleClick), and it is the one to use. It supports as many handlers as you like on the same element and event, it can be removed again later, and it accepts options for the more advanced behaviour in the next lesson. Note that you pass the function, not the result of calling it: handleClick, never handleClick(). Those brackets run the function immediately during setup and then register whatever it returned, which is normally undefined — so nothing happens when you click, and nothing warns you.
function handleClick() {
console.log('clicked');
}
const btn = document.querySelector('#saveBtn');
// 1. HTML attribute - fine for a demo, avoid in real code
// <button onclick="handleClick()">Save</button>
// 2. DOM property - exactly one slot
btn.onclick = handleClick;
btn.onclick = function () { console.log('second'); };
// The first handler is gone. Only 'second' ever prints.
// 3. addEventListener - as many as you like
btn.addEventListener('click', handleClick);
btn.addEventListener('click', () => console.log('also me'));
// Both run, in the order they were added.
// Pass the function; do not call it
btn.addEventListener('click', handleClick); // correct
// btn.addEventListener('click', handleClick()); // wrong - runs it now - Adding the same function reference twice for the same event type registers it only once — the browser ignores the duplicate. Two separate anonymous functions with identical bodies are different references, so both are kept and both will run.
The Event Object
When the browser calls your handler it passes one argument: an event object describing what happened. You do not have to accept it, but almost every handler that does anything useful does. The conventional parameter name is event, shortened to e in brief callbacks.
Every event object carries the same core information. type is the event name. target is the element the event started on. currentTarget is the element whose handler is running right now. timeStamp is when it happened. Different kinds of event then add their own fields — a mouse event has coordinates, a keyboard event has key, an input event gives you the field through target.
The object also carries the two methods that let you interfere with what happens next. preventDefault() cancels the browser's built-in reaction, and stopPropagation() stops the event travelling further up the tree. Propagation is the next lesson's subject; preventDefault has its own section below because you will need it almost immediately.
The fastest way to learn what any event contains is to log it. Put console.log(event) as the first line of a handler, trigger it, then expand the object in the console. Everything the browser knows about that interaction is sitting there, and five minutes of reading it beats memorising a property list you will half-remember.
const btn = document.querySelector('#saveBtn');
const search = document.querySelector('#search');
btn.addEventListener('click', function (event) {
console.log(event.type); // 'click'
console.log(event.target); // the element that was clicked
console.log(event.currentTarget); // the element the handler is on
console.log(event.clientX, event.clientY); // pointer position
console.log(event); // expand this one in the console
});
search.addEventListener('keydown', function (e) {
console.log(e.key); // 'a', 'Enter', 'Escape', 'ArrowDown'
console.log(e.code); // 'KeyA' - the physical key, layout-independent
console.log(e.ctrlKey, e.shiftKey, e.altKey);
}); event.keygives you what the key produced —'a'or'A'depending on Shift.event.codegives you which physical key was pressed, which is what you want for game controls or keyboard shortcuts that must work on any layout.
The Events You Will Actually Use
There are dozens of event types, and in ordinary work you will use perhaps fifteen. They fall into four groups, and knowing the right one for the job saves a great deal of patching later.
Mouse and pointer. click covers almost everything, and it is the accessible choice because it also fires when a user presses Enter or Space on a focused button — which mousedown does not. dblclick, mouseenter and mouseleave handle hover behaviour that CSS alone cannot express. Prefer mouseenter over mouseover: mouseover fires again every time the pointer crosses into a child element, which is what produces flickering menus.
Keyboard. keydown fires when a key goes down and repeats while it is held; keyup fires on release. Use event.key for the character or the name — 'a', 'Enter', 'Escape' — and event.code when you need the physical key regardless of layout. The old keypress event is deprecated and should not be used in new code.
Form. input fires on every change to a field's value, including paste and autofill, and is what you want for live validation. change fires only when the user has finished — leaving the field, or choosing from a select. submit belongs on the form element, not on the button, which is why attaching it to the form also catches the user pressing Enter inside a text field. Window and document. DOMContentLoaded fires when the tree is ready, load when images and stylesheets have finished too, and scroll and resize fire extremely often — those two need throttling, which Lesson 29 covers.
const btn = document.querySelector('#saveBtn');
const card = document.querySelector('.card');
const search = document.querySelector('#search');
const select = document.querySelector('#course');
const form = document.querySelector('#signup');
function closeDialog() {
console.log('dialog closed');
}
// Mouse
btn.addEventListener('click', () => console.log('clicked'));
card.addEventListener('mouseenter', () => card.classList.add('hover'));
card.addEventListener('mouseleave', () => card.classList.remove('hover'));
// Keyboard - Escape closes a dialog, from anywhere on the page
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeDialog();
});
// input fires on every keystroke, and on paste
search.addEventListener('input', (e) => {
console.log('now:', e.target.value);
});
// change fires only once the user has finished
select.addEventListener('change', (e) => {
console.log('chosen:', e.target.value);
});
// submit goes on the form, so Enter in a text field is caught too
form.addEventListener('submit', (e) => {
e.preventDefault();
console.log('submitting');
}); - Use
clickrather thanmousedownfor buttons unless you have a specific reason. Keyboard users and screen readers reach a button throughclick, so building onmousedownquietly excludes anyone not using a mouse.
preventDefault and Default Behaviour
Many elements have built-in behaviour that the browser performs on your behalf. A link navigates. A form submits and reloads the page. A checkbox ticks itself. event.preventDefault() cancels that built-in step while leaving your own handler running normally.
By far the most common use is stopping a form from reloading the page. Without it, the browser submits the form the traditional way, the page reloads, and every variable you were holding is destroyed — which is exactly why a beginner's validation so often appears to "flash and do nothing". The first line of nearly every submit handler you will ever write is event.preventDefault().
Use it deliberately rather than reflexively. Cancelling the default on a link means your JavaScript is now solely responsible for navigation, so if your code throws before it navigates, the link does nothing at all — no error the user can see, no page. Cancelling on a checkbox means the tick is now yours to manage. If you are not replacing the behaviour with something better, leave it alone.
preventDefault and stopPropagation are different things and are constantly confused. preventDefault stops what the browser would have done. stopPropagation stops the event reaching other handlers further up the tree. Neither one implies the other. And returning false from a handler added with addEventListener does nothing whatsoever — that shortcut only ever worked with the old inline attributes.
const form = document.querySelector('#signup');
form.addEventListener('submit', function (event) {
event.preventDefault(); // the page no longer reloads
const email = form.querySelector('#email').value.trim();
if (!email.includes('@')) {
console.log('Invalid email');
return; // early return, nothing below runs
}
console.log('Would send:', email);
});
// A link taken over by JavaScript
document.querySelector('#help').addEventListener('click', function (event) {
event.preventDefault(); // now YOU decide what happens next
console.log('open the help panel');
});
// Returning false here does nothing at all
document.querySelector('#other').addEventListener('click', function () {
return false; // no effect - not a way to cancel anything
}); - Not every default can be cancelled. Some events are marked non-cancellable, and
event.cancelabletells you which — callingpreventDefault()on one of those is simply ignored, so check that property before assuming your call did anything.
target, currentTarget, and this Inside a Handler
Two properties on the event object look interchangeable and are not, and the difference becomes essential the moment you use event delegation in the next lesson.
event.target is the element where the event actually started — the deepest element under the pointer. event.currentTarget is the element whose handler is running at this moment. If you click an icon inside a button, target is the icon and currentTarget is the button. When you attach a handler directly to the thing you care about the two are identical, which is exactly why the distinction stays invisible until it bites.
The practical consequence is worth spelling out. A handler on a button that reads event.target.dataset.id works perfectly until somebody adds an icon or a span inside that button — at which point target is the span, the span has no dataset, and you get undefined. Reading event.currentTarget.dataset.id, or event.target.closest('button'), makes the problem impossible.
Inside a handler added with addEventListener, this is the same as currentTarget — but only when the handler is a regular function. An arrow function has no this of its own and borrows the surrounding one, which is almost never the element. That is the single situation in event code where an arrow function is the wrong choice, and it is a standard interview question. If you like arrows, use event.currentTarget and the question never arises.
// markup: <button id="save"><span class="icon">*</span> Save</button>
const save = document.querySelector('#save');
save.addEventListener('click', function (event) {
console.log(event.target); // the span, if the icon was clicked
console.log(event.currentTarget); // always the button
console.log(this === event.currentTarget); // true - regular function
});
// An arrow function does not get its own this
save.addEventListener('click', (event) => {
console.log(event.currentTarget); // still correct
// console.log(this === event.currentTarget); // false - this came from outside
});
// Robust however deeply nested the click was
save.addEventListener('click', function (event) {
const button = event.target.closest('button');
console.log(button.id); // 'save'
}); addEventListener(type, handler)— the only form to use in real code- Pass the function; adding brackets calls it immediately instead
event.target— where it started;event.currentTarget— where the handler isevent.keyfor the character produced;event.codefor the physical keyinputfires on every keystroke;changefires when the user finishessubmitgoes on the form, never on the buttonevent.preventDefault()stops the browser's default; it does not stop propagation- Use a regular function, not an arrow, if you want
thisto be the element
- When a handler seems not to run, check three things before anything else: did the selector find the element, did you pass the function rather than call it, and did the script run before the element existed. Those three explain the large majority of "my click does nothing" problems.
