Skip to content
JavaScript js dom 5 min read

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 value gives you a string even for <input type="number">. "10" + 5 is "105", not 15. 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.fromEntries keeps only the last value for any repeated field name. For multi-select or checkbox groups that share a name, use data.getAll("tags") to retrieve every value as an array.

The input and change events

Two events report value updates, and the difference matters:

EventFires when…Typical use
inputthe value changes on every keystrokelive search, character counters, instant validation
changethe 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:

MemberReturns / 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.validitya ValidityState object with flags like valueMissing, typeMismatch, patternMismatch, customError
input.setCustomValidity(msg)marks the field invalid with your message; pass "" to clear it
input.validationMessagethe 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 a submit handler when you process data with JavaScript instead of navigating.
  • Use FormData to read whole forms by name rather than querying each control individually.
  • Convert value strings to numbers explicitly (valueAsNumber or Number(...)) before doing arithmetic.
  • Lean on declarative HTML constraints (required, type, pattern, min/max) first; reach for setCustomValidity only for cross-field or dynamic rules.
  • Clear a custom error with setCustomValidity("") as soon as the value becomes valid.
  • Use input for live feedback and change for committed values — pick the one that matches the interaction.
  • Treat client-side validation as UX, not enforcement: re-validate everything on the server.
Last updated June 1, 2026
Was this helpful?