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
form.addEventListener("submit", (event) => {
event.preventDefault(); // stop the page reloading
const data = new FormData(form);
console.log(data.get("email"));
});- Listen for
submiton the form, notclickon 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
| Input | Read with |
|---|---|
| Text, email, number, textarea, select | input.value (always a string) |
| Number as a number | input.valueAsNumber |
| Date as a Date | input.valueAsDate |
| Checkbox | checkbox.checked (true or false) |
| Radio group | form.elements.size.value or FormData.get("size") |
| Multiple select / checkboxes | FormData.getAll("toppings") |
| Files | fileInput.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 method | Does |
|---|---|
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.validationMessage | The 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 (
blurorchange) 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
submiton the form and callpreventDefault()to handle it with JavaScript. input.valueis always a string; usecheckedfor checkboxes andFormDatafor whole forms.checkValidity,validityandsetCustomValiditywork with HTML validation rules.- Link error messages with
aria-describedbyand setaria-invalidfor 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.
Reading a whole form with FormData
Fill in the form and press Send.
<form id="order">
<input name="name" value="Sam" required>
<select name="size"><option>Small</option><option selected>Large</option></select>
<label><input type="checkbox" name="extras" value="cheese" checked> Cheese</label>
<label><input type="checkbox" name="extras" value="olives"> Olives</label>
<label><input type="checkbox" name="gift"> Gift wrap</label>
<button>Send</button>
</form>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));
});Line-by-line explanation
- Line 4Stop the page reloading.
- Line 5Collect every named field.
- Line 9
getAllreturns every ticked box with the same name. - Line 10For a single on/off box,
checkedis clearer. - Line 11
fromEntrieskeeps only one value per name, so usegetAllfor groups.
Custom validation with setCustomValidity
Try mismatched passwords, then matching ones.
<form id="pwform" novalidate>
<label for="pw">Password</label>
<input id="pw" type="password" minlength="8" required aria-describedby="pw-err">
<p id="pw-err" class="err"></p>
<label for="pw2">Confirm password</label>
<input id="pw2" type="password" required aria-describedby="pw2-err">
<p id="pw2-err" class="err"></p>
<button>Save</button>
</form>
<style>.err { color: #b91c1c; min-height: 1.2em; margin: 2px 0 8px; font-size: 14px; } label { display: block; }</style>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!");
});Line-by-line explanation
- Line 7A custom rule joins the browser's own checks; an empty string means valid.
- Lines 9 to 11Show the browser's message next to the field and flag it for screen readers.
- Line 14Check each field when the user leaves it.
- Lines 17 to 20On submit, check everything and move focus to the first problem.
Live feedback while typing
<label for="bio">Short bio</label>
<textarea id="bio" maxlength="80" rows="3" style="width: 100%"></textarea>
<p><span id="count">0</span> / 80 characters</p>
<label for="age">Age</label>
<input id="age" type="number" min="13" max="120">
<p id="age-note"></p>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`;
});Line-by-line explanation
- Lines 3 to 6The
inputevent fires on every keystroke, so the counter updates live. - Line 9
changefires when the user finishes editing the field. - Line 10
valueAsNumbergives a real number, orNaNif the field is empty. - Line 13
validity.rangeUnderflowtells you theminrule failed.
Common JavaScript Forms Mistakes
Listening for click on the submit button
button.addEventListener("click", send); // misses Enter key submitsform.addEventListener("submit", (e) => { e.preventDefault(); send(); });Doing maths with value strings
const total = qty.value + 1; // "2" + 1 = "21"const total = qty.valueAsNumber + 1;
// or Number(qty.value) + 1Never clearing a custom error
if (bad) field.setCustomValidity("Not allowed"); // stays invalid foreverfield.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.
Stop the form from reloading the page.
form.addEventListener("submit", (event) => {
event.();
console.log("Handled with JavaScript");
});
An input has value="5". What is logged?
console.log(qty.value + 2, qty.valueAsNumber + 2);Which property tells you if a checkbox is ticked?
What does input.setCustomValidity("") do?
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.
<form id="f">
<input name="email" value="ana@example.com">
<input type="checkbox" name="terms" checked>
<button id="go">Go</button>
</form>
<p id="out"></p>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}`;
});- Shows the email and ticked state
- Listens for submit
JavaScript Forms Quiz
5 questions to check what you learned.
Which event should you listen for to handle a form being sent?
- submit
- click
- change
- send
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
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
Which event fires on every keystroke in a text field?
- input
- change
- submit
- blur
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:
- While typing in
#email2, it's marked invalid with the message"Emails don't match"if it differs from#email1, and valid otherwise (usesetCustomValidity) - On submit, prevent the reload; if the form is valid, set
#msgtext to`Sent to ${email}`using the value fromFormData - If the form is invalid on submit, set
#msgtext to"Please fix the errors"
<form id="f" novalidate>
<input id="email1" name="email" type="email" required>
<input id="email2" type="email" required>
<button>Send</button>
</form>
<p id="msg"></p>const form = document.querySelector("#f");
const e1 = document.querySelector("#email1");
const e2 = document.querySelector("#email2");
const msg = document.querySelector("#msg");
- Mismatch is invalid
- Match is valid
- Valid submit shows the email
- Invalid submit shows an error