Working with Forms in JavaScript

Read form values, handle submit, validate input with the Constraint Validation API and custom checks, show helpful errors, and send data with FormData.

Lesson 38 of 56Intermediate15 min read

Forms are where websites collect information: contact messages, delivery addresses, search terms, bookings and settings. HTML already does a lot on its own, as you saw in HTML form validation: required, type="email" and minlength stop many mistakes before anything is sent. JavaScript takes forms further.

With JavaScript you can check things HTML can't, such as "the two passwords match" or "the end date is after the start date". You can show friendly error messages next to each field as the person types, send the data in the background with fetch without reloading the page, and update the page with the result.

In this lesson you'll handle the submit event, read values from every kind of input, collect a whole form at once with FormData, use the browser's built-in Constraint Validation API to check fields and set custom messages, and make your error messages accessible to screen reader users.

Handling submit

JavaScript
form.addEventListener("submit", (event) => {
  event.preventDefault();        // stop the page reloading
  const data = new FormData(form);
  console.log(data.get("email"));
});
  • Listen for submit on the form, not click on the button. Submit also fires when someone presses Enter in a field.
  • event.preventDefault() stops the browser's normal behaviour of sending the form and loading a new page, so your code can handle it instead.
  • HTML validation runs before the submit event, so the event only fires if the built-in checks pass (unless the form has novalidate).

Reading values

InputRead with
Text, email, number, textarea, selectinput.value (always a string)
Number as a numberinput.valueAsNumber
Date as a Dateinput.valueAsDate
Checkboxcheckbox.checked (true or false)
Radio groupform.elements.size.value or FormData.get("size")
Multiple select / checkboxesFormData.getAll("toppings")
FilesfileInput.files[0]

FormData reads every named field at once. Object.fromEntries(new FormData(form)) turns it into a plain object, and you can pass the FormData straight to fetch as the request body. Fields need a name attribute to be included.

The Constraint Validation API

Property or methodDoes
form.checkValidity()true if every field is valid
form.reportValidity()Checks and shows the browser's messages
input.validity.valueMissing, .typeMismatch, .tooShort...Which rule failed
input.validationMessageThe browser's message for this field
input.setCustomValidity("message")Marks the field invalid with your message
input.setCustomValidity("")Marks it valid again

setCustomValidity plugs your own rules, such as matching passwords, into the browser's system, so they behave just like required. Always clear the message with an empty string once the field is fixed, or the form stays invalid forever.

Accessible error messages

  • Put the error text in an element next to the field, and link it with aria-describedby="email-error" so screen readers read it with the field.
  • Set aria-invalid="true" on fields with errors.
  • Show errors after the user leaves a field (blur or change) or submits, not on the first keystroke.
  • On submit, move focus to the first invalid field with field.focus().
  • Say what to do, not just what's wrong: "Enter an email like name@example.com" rather than "Invalid input".

JavaScript Forms Syntax

Form handling

form.addEventListener("submit", (e) => {
  e.preventDefault();
  if (!form.checkValidity()) return form.reportValidity();
  const data = Object.fromEntries(new FormData(form));
});
confirm.addEventListener("input", () => {
  confirm.setCustomValidity(confirm.value === pw.value ? "" : "Passwords don't match");
});

JavaScript Forms Summary

  • Listen for submit on the form and call preventDefault() to handle it with JavaScript.
  • input.value is always a string; use checked for checkboxes and FormData for whole forms.
  • checkValidity, validity and setCustomValidity work with HTML validation rules.
  • Link error messages with aria-describedby and set aria-invalid for screen readers.
  • Always validate again on the server.

JavaScript Forms Examples

Each example runs in the page. Edit the code and press Run to see what changes.

Example 1

Reading a whole form with FormData

Fill in the form and press Send.

const form = document.querySelector("#order");

form.addEventListener("submit", (e) => {
  e.preventDefault();
  const data = new FormData(form);

  console.log("name:", data.get("name"));
  console.log("size:", data.get("size"));
  console.log("extras:", data.getAll("extras"));
  console.log("gift:", form.elements.gift.checked);
  console.log("as object:", Object.fromEntries(data));
});
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 4Stop the page reloading.
  2. Line 5Collect every named field.
  3. Line 9getAll returns every ticked box with the same name.
  4. Line 10For a single on/off box, checked is clearer.
  5. Line 11fromEntries keeps only one value per name, so use getAll for groups.
Example 2

Custom validation with setCustomValidity

Try mismatched passwords, then matching ones.

const form = document.querySelector("#pwform");
const pw = document.querySelector("#pw");
const pw2 = document.querySelector("#pw2");

function check(field) {
  if (field === pw2) {
    pw2.setCustomValidity(pw2.value && pw2.value !== pw.value ? "Passwords don't match" : "");
  }
  const error = document.getElementById(field.getAttribute("aria-describedby"));
  error.textContent = field.validity.valid ? "" : field.validationMessage;
  field.setAttribute("aria-invalid", String(!field.validity.valid));
}

for (const field of [pw, pw2]) field.addEventListener("blur", () => check(field));

form.addEventListener("submit", (e) => {
  e.preventDefault();
  [pw, pw2].forEach(check);
  const firstBad = form.querySelector(":invalid");
  if (firstBad) return firstBad.focus();
  console.log("Saved!");
});
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 7A custom rule joins the browser's own checks; an empty string means valid.
  2. Lines 9 to 11Show the browser's message next to the field and flag it for screen readers.
  3. Line 14Check each field when the user leaves it.
  4. Lines 17 to 20On submit, check everything and move focus to the first problem.
Example 3

Live feedback while typing

const bio = document.querySelector("#bio");
const count = document.querySelector("#count");
bio.addEventListener("input", () => {
  count.textContent = bio.value.length;
  count.style.color = bio.value.length > 70 ? "#b45309" : "";
});

const age = document.querySelector("#age");
age.addEventListener("change", () => {
  const n = age.valueAsNumber;
  document.querySelector("#age-note").textContent =
    Number.isNaN(n) ? "Please enter a number" :
    age.validity.rangeUnderflow ? "You must be at least 13" :
    `Thanks, ${n} it is`;
});
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 3 to 6The input event fires on every keystroke, so the counter updates live.
  2. Line 9change fires when the user finishes editing the field.
  3. Line 10valueAsNumber gives a real number, or NaN if the field is empty.
  4. Line 13validity.rangeUnderflow tells you the min rule failed.

Common JavaScript Forms Mistakes

Listening for click on the submit button

Don't
button.addEventListener("click", send); // misses Enter key submits
Do
form.addEventListener("submit", (e) => { e.preventDefault(); send(); });

Doing maths with value strings

Don't
const total = qty.value + 1; // "2" + 1 = "21"
Do
const total = qty.valueAsNumber + 1;
// or Number(qty.value) + 1

Never clearing a custom error

Don't
if (bad) field.setCustomValidity("Not allowed"); // stays invalid forever
Do
field.setCustomValidity(bad ? "Not allowed" : "");

JavaScript Forms in Real Projects

Forms in production

Checkout pages validate card details as you type and highlight the first error on submit. Booking sites check that the return date is after the departure date with custom validity. Most sites send forms with fetch and FormData, then show a success message without reloading. Libraries like React Hook Form and Zod build on these same browser APIs.

JavaScript Forms Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Stop the form from reloading the page.

form.addEventListener("submit", (event) => {
  event.();
  console.log("Handled with JavaScript");
});
Predict the outputmedium

An input has value="5". What is logged?

console.log(qty.value + 2, qty.valueAsNumber + 2);
Multiple choiceeasy

Which property tells you if a checkbox is ticked?

Multiple choicemedium

What does input.setCustomValidity("") do?

Fix the bugmedium

On submit, the form should not reload, and #out should show the email and whether "terms" is ticked, like ana@example.com true. Fix it.

const form = document.querySelector("#f");
document.querySelector("#go").addEventListener("click", () => {
  const data = new FormData(form);
  document.querySelector("#out").textContent = `${data.email} ${form.elements.terms.value}`;
});
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • Shows the email and ticked state
  • Listens for submit

JavaScript Forms Quiz

5 questions to check what you learned.

  1. Which event should you listen for to handle a form being sent?

    • submit
    • click
    • change
    • send
  2. Why must form fields have a name to appear in FormData?

    • For styling
    • FormData uses the name attribute as each value's key
    • Names make fields required
    • It's only needed for checkboxes
  3. How do you connect an error message to a field for screen readers?

    • Put it in the placeholder
    • Give the field aria-describedby with the message element's id
    • Use alert()
    • Colour the border red
  4. Which event fires on every keystroke in a text field?

    • input
    • change
    • submit
    • blur
  5. Why validate again on the server?

    • Browsers can't validate
    • It's faster
    • Client-side checks can be bypassed, so the server must not trust incoming data
    • Only servers can check emails

JavaScript Forms Challenge

A matching-email form

Complete the form logic so that:

  1. While typing in #email2, it's marked invalid with the message "Emails don't match" if it differs from #email1, and valid otherwise (use setCustomValidity)
  2. On submit, prevent the reload; if the form is valid, set #msg text to `Sent to ${email}` using the value from FormData
  3. If the form is invalid on submit, set #msg text to "Please fix the errors"
const form = document.querySelector("#f");
const e1 = document.querySelector("#email1");
const e2 = document.querySelector("#email2");
const msg = document.querySelector("#msg");
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • Mismatch is invalid
  • Match is valid
  • Valid submit shows the email
  • Invalid submit shows an error