Knowledge Base/guides/Form submit Events and preventDefault in JavaScript

Form submit Events and preventDefault in JavaScript

Form submit events and preventDefault in JavaScript are not just a beginner ritual before "real" form work starts. They decide whether the browser owns the submission or your code owns it. If you skip that boundary, a form can reload the page before your validation message, API request, loading state, or profile preview has a chance to matter.

The useful pattern is: listen on the form, prevent the default action, read the current values, validate them, then update the page or send the data.

What the submit event is

The submit event fires on a <form> when the form is submitted. That can happen when a user clicks a submit button, presses Enter inside a text field, or triggers the form from assistive technology.

That is why the listener belongs on the form, not only on the button:

<form id="signup-form">
  <label>
    Email
    <input id="email" name="email" type="email" />
  </label>
  <button type="submit">Create account</button>
</form>
const form = document.querySelector("#signup-form");

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

  console.log("The form was submitted");
});

A click listener on the button only handles button clicks. A submit listener on the form handles the whole form action. That is the version you usually want.

For a guided version of this idea, see the Forms and Input Events lesson.

Why forms reload by default

HTML forms existed before JavaScript-heavy interfaces. Their built-in job is to collect successful form controls, send the data to the URL in the form's action, and navigate to the response.

<form action="/newsletter" method="post">
  <input name="email" />
  <button type="submit">Subscribe</button>
</form>

Without JavaScript, that is good behavior. The browser can submit the form, load the next page, and preserve a basic working experience.

In a JavaScript-controlled interface, that default navigation is often the wrong behavior. Imagine a settings panel that should show "Saved" inline, a search form that should update results without losing the page, or a profile editor that should validate fields before sending anything. A full page reload would interrupt the flow and reset the current UI state.

That is the problem preventDefault() solves.

What preventDefault changes

event.preventDefault() cancels the browser's built-in action for a cancelable event. For a form submit, that means the browser does not navigate away automatically.

const form = document.querySelector("#signup-form");
const status = document.querySelector("#signup-status");

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

  status.textContent = "Submitting...";
});

The event still happened. Your listener still runs. Other submit listeners may still run too. preventDefault() only says: do not continue with the form's default submit action.

This same idea appears with links. A link click normally navigates. In the Default Actions exercise, preventDefault() stops that navigation so the page can show an inline preview instead.

Read values at submit time

Form controls keep their current values in DOM properties. Text inputs use .value. Checkboxes use .checked. Select elements usually use .value.

<form id="invite-form">
  <label>
    Teammate email
    <input id="teammate-email" type="email" />
  </label>
  <label>
    <input id="send-copy" type="checkbox" />
    Send me a copy
  </label>
  <button type="submit">Send invite</button>
</form>
<p id="invite-summary"></p>
const form = document.querySelector("#invite-form");
const emailInput = document.querySelector("#teammate-email");
const copyCheckbox = document.querySelector("#send-copy");
const summary = document.querySelector("#invite-summary");

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

  const email = emailInput.value.trim();
  const shouldSendCopy = copyCheckbox.checked;

  summary.textContent = shouldSendCopy
    ? `Invite queued for ${email}. You will receive a copy.`
    : `Invite queued for ${email}.`;
});

Notice the timing. The handler reads the field values when the user submits, not when the page first loads. That is why this works after the user edits the field.

You can practice that exact submit-time read in the Form Values exercise.

Use FormData for several fields

When a form has meaningful name attributes, FormData can read the submitted values from the form element itself.

<form id="profile-form">
  <label>
    Display name
    <input name="displayName" />
  </label>
  <label>
    Role
    <select name="role">
      <option value="Designer">Designer</option>
      <option value="Developer">Developer</option>
      <option value="Manager">Manager</option>
    </select>
  </label>
  <button type="submit">Save profile</button>
</form>
<p id="profile-summary"></p>
const form = document.querySelector("#profile-form");
const summary = document.querySelector("#profile-summary");

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

  const data = new FormData(form);
  const displayName = String(data.get("displayName") || "").trim();
  const role = String(data.get("role") || "").trim();

  summary.textContent = `${displayName} - ${role}`;
});

This keeps extraction tied to the form markup. If the form grows from two fields to five, you do not need a separate query for every field unless you need direct access for focus, classes, or custom validation state.

Try this pattern in the FormData Summary exercise.

Where validation fits

Validation belongs after preventDefault() and before the success work.

const form = document.querySelector("#project-form");
const projectName = document.querySelector("#project-name");
const error = document.querySelector("#project-error");
const confirmation = document.querySelector("#project-confirmation");

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

  const name = projectName.value.trim();

  if (name === "") {
    error.textContent = "Project name is required";
    projectName.setAttribute("aria-invalid", "true");
    confirmation.textContent = "";
    return;
  }

  error.textContent = "";
  projectName.setAttribute("aria-invalid", "false");
  confirmation.textContent = `Created ${name}`;
});

The order matters:

  1. Stop the default navigation.
  2. Normalize the value with trim().
  3. Show field-specific feedback if the value is invalid.
  4. Return early so the success code does not run.
  5. Clear previous errors before showing the success state.

That early return is not decorative. It keeps invalid data from leaking into the rest of the handler. The Form Validation exercise focuses on this exact branch.

The realistic form flow

A real form handler usually has a small transaction shape. It reads, validates, updates state, and then resets only the pieces that should reset.

<form id="task-form">
  <label>
    New task
    <input id="task-title" />
  </label>
  <button type="submit">Add task</button>
  <p id="task-error" role="alert"></p>
</form>

<ul id="task-list"></ul>
const form = document.querySelector("#task-form");
const titleInput = document.querySelector("#task-title");
const error = document.querySelector("#task-error");
const list = document.querySelector("#task-list");

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

  const title = titleInput.value.trim();

  if (title === "") {
    error.textContent = "Enter a task title";
    titleInput.setAttribute("aria-invalid", "true");
    return;
  }

  const item = document.createElement("li");
  item.textContent = title;
  list.append(item);

  titleInput.value = "";
  titleInput.setAttribute("aria-invalid", "false");
  error.textContent = "";
});

This is the shape behind many small app features: todo forms, tag editors, invite screens, admin filters, profile summaries, and checkout notes. The form submit event gives you one moment where the user's intent is clear: they are done editing this set of fields and want the action to happen.

Use textContent for the submitted title because it is user-provided text. If you put submitted values into innerHTML, the browser parses them as markup. The textContent vs innerHTML vs innerText guide explains that boundary in more detail.

Common pitfalls

Listening on the button

This misses keyboard submit paths:

const button = document.querySelector("#save-button");

button.addEventListener("click", () => {
  console.log("Saved");
});

Prefer the form submit event:

const form = document.querySelector("#settings-form");

form.addEventListener("submit", (event) => {
  event.preventDefault();
  console.log("Saved");
});

Forgetting that buttons can submit

Inside a form, a plain <button> acts like type="submit" in HTML.

<form id="editor">
  <input id="title" />
  <button id="preview-button">Preview</button>
  <button type="submit">Publish</button>
</form>

If Preview should not submit the form, give it an explicit type:

<button id="preview-button" type="button">Preview</button>

Use type="submit" for the button that should submit the form. Use type="button" for buttons that only run JavaScript behavior inside the form.

Reading values before the user changes them

This captures the starting value, not the submitted value:

const emailInput = document.querySelector("#email");
const email = emailInput.value;

form.addEventListener("submit", (event) => {
  event.preventDefault();
  console.log(email);
});

Read inside the handler:

form.addEventListener("submit", (event) => {
  event.preventDefault();
  console.log(emailInput.value);
});

Validating raw whitespace

input.value === "" misses values such as " ". Most required text fields should validate the trimmed value:

const name = nameInput.value.trim();

if (name === "") {
  error.textContent = "Name is required";
  return;
}

Treating preventDefault like stopPropagation

preventDefault() does not stop bubbling. It cancels the default browser action. If you need to understand how an event moves through parent elements, read the guide to event bubbling, target, and currentTarget.

When not to prevent default

Do not cancel the default submit just because you can. Native form submission is still useful when the server should handle the whole request and navigation is expected.

For example, a simple email signup page can work perfectly with:

<form action="/newsletter" method="post">
  <input name="email" type="email" required />
  <button type="submit">Subscribe</button>
</form>

That form does not need JavaScript to be valid. Add a submit handler when your page needs custom validation, inline rendering, async submission, analytics, confirmation UI, or some other client-side behavior before navigation.

Key Takeaways

  • The submit listener belongs on the form because it catches button clicks, Enter key submits, and other form submission paths.
  • A form reloads by default because native HTML form submission navigates to a response from the server.
  • event.preventDefault() cancels that default navigation so JavaScript can validate, render feedback, or send data intentionally.
  • Read input values inside the submit handler so you get the current submitted values, not the initial page values.
  • Validate normalized values before success work, and use textContent when rendering user-provided form data.
Share this article:
javascriptdomformsevents