HTML events: what they are and how to handle them

An event is a signal the browser fires when something happens on the page, such as a click, a key press or a form send. Your JavaScript decides what to do when it arrives.

In web pages, an event is a signal the browser fires when something happens: a click, a key press, a form send, a finished page load. HTML events here means these browser events, not an event invitation page (for that, see event landing page).

You react to an event with a handler, a function the browser calls.

Start with the shortest way: put the handler in an HTML attribute. Click, type and pick an option below.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HTML events</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 12px; }
  button, input, select { font: inherit; padding: 8px 12px; border: 1px solid #c9ced8; border-radius: 8px; background: #fff; }
  button { cursor: pointer; background: #2563eb; border-color: #2563eb; color: #fff; }
  input { width: 170px; max-width: 100%; box-sizing: border-box; }
  #log { margin: 0; padding: 10px 12px; min-height: 96px; background: #fff; border: 1px solid #e1e4ea; border-radius: 8px;
         font: 13px/1.6 ui-monospace, Consolas, monospace; list-style: none; }
  #log li:first-child { font-weight: 700; color: #0f5132; }
</style>
</head>
<body>
<div class="row">
  <!-- each on... attribute holds JavaScript that runs when that event fires -->
  <button onclick="log('click on ' + event.target.tagName)">Click me</button>
  <input placeholder="Type here" oninput="log('input: ' + this.value)">
  <select onchange="log('change: ' + this.value)">
    <option>red</option><option>green</option><option>blue</option>
  </select>
</div>
<ul id="log"><li>Events appear here, newest first.</li></ul>

<script>
  const list = document.getElementById('log');
  function log(text) {
    const li = document.createElement('li');
    li.textContent = text;
    list.prepend(li);
    while (list.children.length > 5) list.lastChild.remove();  // keep the last five
  }
</script>
</body>
</html>
Three elements, three event attributes: onclick, oninput and onchange. Each one logs what fired.

Each on... attribute holds JavaScript. Inside it, event is the event object and this is the element. The rest of this guide covers what the browser sends, the other way to attach handlers, and the mistakes to avoid.

How an event reaches your code

An event has three steps. Something happens, the browser creates an event object that describes it, and your handler runs if you attached one for that type.

From action to handler: something happens, the browser fires an event object, your function runs.
From action to handler: something happens, the browser fires an event object, your function runs.

Events are not only for people. MDN notes that an event can come from a user action, from an API reporting the progress of an asynchronous task, or from your own code, for example by calling element.click().

Event attributes or addEventListener

There are two places a handler can live. The attribute sits in the HTML. The listener is attached from a script.

The same click handler written as an HTML attribute and as a script listener.
The same click handler written as an HTML attribute and as a script listener.

MDN's events guide recommends addEventListener and advises against inline attributes. The attribute mixes HTML with JavaScript and becomes hard to manage across many elements. Assigning to el.onclick has a limit of its own: you cannot add more than one handler for the same event.

onclick="..." addEventListener
Written in The HTML tag A <script>
Several handlers per event No Yes
Good for A quick test Pages you will keep editing

addEventListener in JavaScript covers its options, bubbling phases and removing listeners.

The events you will use most

These are the events used in this guide.

Event Fires when Worth knowing
click A button is pressed and released, or tapped Also fires for Space or Enter on a focused button
input A field value changes as the user edits Runs on every keystroke in a text field
change A value is committed Text fields: when focus leaves. Checkbox, select: at once
submit A form is submitted Fires on the <form>, not on the button
keydown A key is pressed Bubbles; read e.key for which key
DOMContentLoaded The HTML is fully parsed Does not wait for images

Two details save debugging time. The submit event is not sent when a script calls form.submit(); form.requestSubmit() does send it. And DOMContentLoaded is not the same as load, which waits for the whole page, images included.

The order events fire in

One action often fires several events. A mouse click is a good case: click comes after mousedown and mouseup, in that order. Typing and leaving a field shows the input and change difference.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Event order</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 12px; }
  button, input { font: inherit; padding: 8px 12px; border: 1px solid #c9ced8; border-radius: 8px; background: #fff; }
  #target { background: #2563eb; border-color: #2563eb; color: #fff; cursor: pointer; }
  input[type=text] { width: 160px; max-width: 100%; box-sizing: border-box; }
  label { display: inline-flex; align-items: center; gap: 6px; }
  #log { margin: 0 0 8px; padding: 10px 12px; height: 210px; overflow-y: auto; background: #fff; border: 1px solid #e1e4ea;
         border-radius: 8px; font: 13px/1.65 ui-monospace, Consolas, monospace; list-style: none; }
  #log li span { color: #6b7280; }
  .t-click, .t-change { color: #0f5132; font-weight: 700; }
  #clear { background: #fff; color: #1d2330; }
</style>
</head>
<body>
<div class="row">
  <button id="target">Click me</button>
  <input type="text" id="field" placeholder="Type here">
  <label><input type="checkbox" id="box"> tick me</label>
</div>
<ol id="log"></ol>
<button id="clear">Clear log</button>

<script>
  const log = document.getElementById('log');
  const types = ['mousedown', 'mouseup', 'click', 'keydown', 'input', 'change'];
  let n = 0;

  // attach the same logger to three elements, once per event type
  for (const id of ['target', 'field', 'box']) {
    const el = document.getElementById(id);
    for (const type of types) {
      el.addEventListener(type, (e) => {
        const li = document.createElement('li');
        li.className = 't-' + e.type;
        li.innerHTML = ++n + '. ' + e.type + ' <span>on #' + e.currentTarget.id + '</span>';
        log.append(li);
        log.scrollTop = log.scrollHeight;
      });
    }
  }
  document.getElementById('clear').addEventListener('click', () => { log.innerHTML = ''; n = 0; });
</script>
</body>
</html>
Click the button, then type in the field and tick the box. The log shows every event in the order it fired.

Try this: type two letters, then click the checkbox. The field logs one input per key, and its single change appears only when focus moves away.

The event object, preventDefault and delegation

Every handler receives an event object. Three parts are worth learning first.

  • e.target is the element the event happened on, such as the button you clicked.
  • e.currentTarget is the element whose handler is running now. It changes as the event bubbles up through parents.
  • e.preventDefault() cancels the browser's default action for that event, such as following a link or sending a form.

stopPropagation() is a different tool. It stops the event travelling further up the page, but it does not cancel the default action.

Bubbling gives you event delegation: instead of a listener on every child, put one on the parent and check e.target. Items added later are covered too, because their events also bubble up.

list.addEventListener('click', (e) => {
  const item = e.target.closest('li');
  if (item) console.log('clicked', item.textContent);
});

The mistake that runs the handler on page load

A frequent first bug is adding parentheses. save() is a call, so the function runs while the page loads, and what it returns is what gets attached.

Left: save() runs once at load and click does nothing. Right: save is handed to the browser and runs on every click.
Left: save() runs once at load and click does nothing. Right: save is handed to the browser and runs on every click.

The fix is to pass the function without parentheses. If you need to give it arguments, wrap it: () => save(5).

A finished example: a feedback form

This form uses five events together: click on rating buttons through one delegated listener, input for a live counter, change for a checkbox, keydown for a Ctrl+Enter shortcut, and submit.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Feedback form with events</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  form { max-width: 420px; background: #fff; border: 1px solid #e1e4ea; border-radius: 12px; padding: 16px; }
  h2 { margin: 0 0 10px; font-size: 17px; }
  .stars { display: flex; gap: 6px; margin-bottom: 12px; }
  .stars button { width: 40px; height: 40px; border: 1px solid #c9ced8; border-radius: 8px; background: #fff; font: inherit; cursor: pointer; }
  .stars button[aria-pressed="true"] { background: #2563eb; border-color: #2563eb; color: #fff; }
  textarea { width: 100%; box-sizing: border-box; height: 80px; padding: 8px; font: inherit; border: 1px solid #c9ced8; border-radius: 8px; resize: vertical; }
  .meta { display: flex; justify-content: space-between; font-size: 13px; color: #6b7280; margin: 4px 0 10px; }
  .meta.warn { color: #9a3412; }
  .send { margin-top: 10px; padding: 9px 16px; font: inherit; border: 0; border-radius: 8px; background: #0f5132; color: #fff; cursor: pointer; }
  pre { margin: 12px 0 0; padding: 10px; background: #f4f5f7; border-radius: 8px; font: 12.5px/1.5 ui-monospace, Consolas, monospace; white-space: pre-wrap; min-height: 20px; }
</style>
</head>
<body>
<form id="form">
  <h2>How was it?</h2>
  <!-- one click listener on the parent handles all five buttons -->
  <div class="stars" id="stars" role="group" aria-label="Rating">
    <button type="button" data-rating="1" aria-pressed="false">1</button>
    <button type="button" data-rating="2" aria-pressed="false">2</button>
    <button type="button" data-rating="3" aria-pressed="false">3</button>
    <button type="button" data-rating="4" aria-pressed="false">4</button>
    <button type="button" data-rating="5" aria-pressed="false">5</button>
  </div>
  <input type="hidden" name="rating" id="rating" value="">
  <textarea name="comment" id="comment" maxlength="120" placeholder="Comment (Ctrl+Enter sends)" required></textarea>
  <div class="meta" id="meta"><span id="count">0 / 120</span><label><input type="checkbox" name="contact" id="contact"> You may contact me</label></div>
  <button class="send" type="submit">Send</button>
  <pre id="out">Nothing sent yet.</pre>
</form>

<script>
  const form = document.getElementById('form');
  const comment = document.getElementById('comment');

  // 1. click: delegation - e.target is the button that was pressed
  document.getElementById('stars').addEventListener('click', (e) => {
    const btn = e.target.closest('button[data-rating]');
    if (!btn) return;
    document.getElementById('rating').value = btn.dataset.rating;
    for (const b of e.currentTarget.children) b.setAttribute('aria-pressed', b === btn);
  });

  // 2. input: runs on every keystroke
  comment.addEventListener('input', () => {
    document.getElementById('count').textContent = comment.value.length + ' / 120';
  });

  // 3. change: runs when the checkbox is toggled
  document.getElementById('contact').addEventListener('change', (e) => {
    document.getElementById('meta').classList.toggle('warn', e.target.checked);
  });

  // 4. keydown: Ctrl+Enter (or Cmd+Enter) sends
  comment.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
      e.preventDefault();
      form.requestSubmit();  // fires the submit event, like clicking Send
    }
  });

  // 5. submit: stop the page reload and show the data instead
  form.addEventListener('submit', (e) => {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(form));
    document.getElementById('out').textContent = JSON.stringify(data, null, 2);
  });
</script>
</body>
</html>
Pick a rating, type a comment, then press Send or Ctrl+Enter. The form data is shown on the page, and nothing is sent anywhere.

What each part does:

  • Delegation: one click listener on the button row finds the pressed button with e.target.closest().
  • Live counter: the input listener updates the count after every keystroke.
  • Shortcut: keydown checks e.key and e.ctrlKey, then calls form.requestSubmit(), which fires the same submit event as the button.
  • Submit: preventDefault() stops the page from reloading, and FormData reads the fields. Form submit in HTML goes deeper.

When it does not work

What you see Cause Fix
The handler runs once on load, never on click Parentheses: handler() was passed Pass handler, or () => handler()
A console error about null The script ran before the element existed Put the script at the end of body, or use defer
Nothing happens on click The handler name has a typo, or the script has an earlier error Open the console and read the first error
The form reloads the page submit was not cancelled Call e.preventDefault() in the handler
A submit listener never runs The listener is on the button, or the form failed validation Listen on the <form>; check required fields
A new item has no click handler The listener was added to each item at load Delegate from the parent list
Enter does not click a div Only elements with a default key action do that Use a real <button>

If the script does not run at all, HTML JavaScript not working goes through the causes one by one.

Events only show their worth when someone clicks. A screenshot of the form cannot be tried, and an .html attachment may open as plain code on a phone.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can click the buttons and type in the form themselves. If you change the code later, the same link shows the new version.

Questions people ask

What are events in HTML?

They are signals the browser fires when something happens: a click, a key press, a form submit, a page finishing its load. HTML names the events and lets you write a handler in an attribute such as onclick, but the handler itself is JavaScript.

Can I use HTML events without JavaScript?

No. An attribute such as onclick holds JavaScript code, and the browser runs that code when the event fires. For a few jobs, plain HTML elements such as a form or a link already do the work without any handler.

Should I use the onclick attribute or addEventListener?

MDN's guide recommends addEventListener and advises against inline attributes, because they mix HTML with JavaScript and are hard to manage across many elements. An attribute is fine for a quick test.

What is the difference between the input and change events?

For a text field, input fires as the user edits the value, so it runs on every keystroke. change fires when the value is committed, for example when the field loses focus. For a checkbox or a select, change fires as soon as the choice is made.

Why does my handler run when the page loads instead of on click?

You wrote handler() with parentheses, which calls the function right away. Pass the function itself, handler, and the browser will call it each time the event fires.

Keep reading