Forms & Inputs
Forms are where most real user input enters a web app — logins, search boxes, checkout flows, settings panels. JavaScript lets you read what a user typed, react to changes as they happen, intercept the submit so the page never reloads, and validate everything before it leaves the browser. This page covers reading input values, the submit event and preventDefault, the FormData API, the input and change events, and the built-in constraint validation API.
Reading input values
Every form control exposes its current value through a property. For text-like inputs, <textarea>, and <select>, that property is value and it is always a string. Checkboxes and radio buttons use checked (a boolean) instead.
const email = document.querySelector("#email").value; // string
const agreed = document.querySelector("#terms").checked; // boolean
const plan = document.querySelector("#plan").value; // selected <option> value
Because value is always a string, coerce it explicitly when you need a number. Use Number(input.value) or the input’s own valueAsNumber property, which returns NaN for empty or invalid entries.
const qty = document.querySelector("#qty").valueAsNumber; // number, NaN if blank
Gotcha: Reading
valuegives you a string even for<input type="number">."10" + 5is"105", not15. Always convert before doing math.
Handling submission
When a form is submitted — by clicking a submit button or pressing Enter in a field — the browser fires a submit event on the <form> element and then, by default, navigates the page. In a single-page app you almost always want to stop that navigation with event.preventDefault() and handle the data yourself.
const form = document.querySelector("#signup");
form.addEventListener("submit", (event) => {
event.preventDefault(); // stop the page reload
const email = form.elements.email.value;
console.log("Submitting:", email);
});
Note form.elements — a live collection of all controls, accessible by their name or id. It’s a clean way to reach fields without a separate querySelector for each one.
Collecting data with FormData
Reading fields one by one gets tedious. The FormData constructor reads an entire form in one step, keyed by each control’s name attribute. It is the idiomatic way to gather form state and pairs perfectly with fetch.
form.addEventListener("submit", async (event) => {
event.preventDefault();
const data = new FormData(form);
console.log(data.get("email")); // single field
console.log([...data.entries()]); // every name/value pair
// Send it straight to a server — no manual JSON building
await fetch("/api/signup", { method: "POST", body: data });
});
To send JSON instead of multipart form data, convert it with Object.fromEntries:
const data = new FormData(form);
const payload = Object.fromEntries(data);
await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
Tip:
Object.fromEntrieskeeps only the last value for any repeated field name. For multi-select or checkbox groups that share a name, usedata.getAll("tags")to retrieve every value as an array.
The input and change events
Two events report value updates, and the difference matters:
| Event | Fires when… | Typical use |
|---|---|---|
input | the value changes on every keystroke | live search, character counters, instant validation |
change | the value is committed (blur for text, immediately for checkboxes/selects) | “save on done”, final validation |
const search = document.querySelector("#search");
// Runs on each keystroke
search.addEventListener("input", (event) => {
console.log("Live value:", event.target.value);
});
// Runs once the field loses focus with a new value
search.addEventListener("change", (event) => {
console.log("Committed:", event.target.value);
});
The live demo below reads input, prevents the default submit, and reflects the data back without a reload.
<form id="profile">
<input name="username" placeholder="Username" required />
<input name="email" type="email" placeholder="Email" required />
<label><input name="newsletter" type="checkbox" /> Subscribe</label>
<button type="submit">Save</button>
</form>
<pre id="out">Fill the form and submit…</pre>
<script>
const form = document.querySelector("#profile");
const out = document.querySelector("#out");
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form);
out.textContent = JSON.stringify(Object.fromEntries(data), null, 2);
});
</script>
The constraint validation API
HTML attributes like required, type="email", min, max, pattern, and maxlength give you declarative validation for free — the browser blocks submission and shows a native message. JavaScript exposes the same engine so you can check validity on demand and add your own rules.
The key methods live on every form control:
| Member | Returns / does |
|---|---|
input.checkValidity() | true if the field passes all constraints, else false (and fires an invalid event) |
input.reportValidity() | same check, but also displays the native error bubble |
input.validity | a ValidityState object with flags like valueMissing, typeMismatch, patternMismatch, customError |
input.setCustomValidity(msg) | marks the field invalid with your message; pass "" to clear it |
input.validationMessage | the current error text the browser would show |
Use setCustomValidity for rules HTML can’t express — like confirming two passwords match. Crucially, you must clear it with an empty string once the value becomes valid, or the field stays “invalid” forever.
const password = document.querySelector("#password");
const confirm = document.querySelector("#confirm");
confirm.addEventListener("input", () => {
if (confirm.value !== password.value) {
confirm.setCustomValidity("Passwords must match");
} else {
confirm.setCustomValidity(""); // valid again — clear it
}
});
This pen ties it together: native constraints plus a custom rule, surfaced with reportValidity.
<form id="reg" novalidate>
<input id="age" name="age" type="number" min="18" placeholder="Age (18+)" required />
<input id="code" name="code" placeholder="Code: ABC-123" pattern="[A-Z]{3}-\d{3}" required />
<button type="submit">Register</button>
</form>
<p id="status"></p>
<script>
const form = document.querySelector("#reg");
const status = document.querySelector("#status");
form.addEventListener("submit", (event) => {
event.preventDefault();
if (form.checkValidity()) {
status.textContent = "✅ All fields valid!";
} else {
status.textContent = "❌ Please fix the highlighted fields.";
form.reportValidity(); // show native bubbles
}
});
</script>
Inspecting validity lets you tailor messages to the exact failure:
const age = document.querySelector("#age");
if (!age.checkValidity()) {
if (age.validity.valueMissing) console.log("Age is required");
else if (age.validity.rangeUnderflow) console.log("Must be 18 or older");
}
Output:
Must be 18 or older
Note: Client-side validation improves UX but is never a security boundary — a determined user can bypass it. Always re-validate on the server.
Best Practices
- Always call
event.preventDefault()in asubmithandler when you process data with JavaScript instead of navigating. - Use
FormDatato read whole forms bynamerather than querying each control individually. - Convert
valuestrings to numbers explicitly (valueAsNumberorNumber(...)) before doing arithmetic. - Lean on declarative HTML constraints (
required,type,pattern,min/max) first; reach forsetCustomValidityonly for cross-field or dynamic rules. - Clear a custom error with
setCustomValidity("")as soon as the value becomes valid. - Use
inputfor live feedback andchangefor committed values — pick the one that matches the interaction. - Treat client-side validation as UX, not enforcement: re-validate everything on the server.