ESC

Type to search the knowledge base.

Form Validation Attributes

Native constraint validation — required, type, pattern, min/max, minlength/maxlength, and wiring :invalid with accessible errors.

beginner3 min read
  • html
  • form-validation
  • forms

Browsers ship a constraint validation API. Attributes like required, type="email", min, max, minlength, maxlength, pattern, and step define rules; submit attempts can block and show UA messages — or you customize with JS while keeping the same engine.

Docs: MDN constraint validation, MDN forms.

Core attributes

<form novalidate id="signup">
  <!-- novalidate if you fully custom-handle; otherwise leave UA validation on -->
</form>

<label for="email">Email</label>
<input
  id="email"
  name="email"
  type="email"
  required
  autocomplete="email"
  aria-describedby="email-hint email-error"
/>
<span id="email-hint">We’ll send a confirmation link.</span>
<span id="email-error" role="alert" hidden></span>

<label for="age">Age</label>
<input id="age" name="age" type="number" min="13" max="120" step="1" required />

<label for="username">Username</label>
<input
  id="username"
  name="username"
  required
  minlength="3"
  maxlength="30"
  pattern="[a-zA-Z0-9_]+"
  title="Letters, numbers, and underscore only"
/>
Attribute Role
required Non-empty (details vary by type)
type Built-in syntax (email, url, number, …)
min / max Bounds for number/date/range
step Allowed increments
minlength / maxlength Text length
pattern Regex against value (anchored per rules)
multiple Comma-separated emails, etc.

Checking validity in JS

const form = document.querySelector("#signup");
const email = form.elements.email;

form.addEventListener("submit", (e) => {
  if (!form.checkValidity()) {
    e.preventDefault();
    // show errors
  }
});

email.addEventListener("blur", () => {
  if (!email.validity.valid) {
    showError(email, messageFor(email));
  }
});

function messageFor(input) {
  if (input.validity.valueMissing) return "This field is required.";
  if (input.validity.typeMismatch) return "Enter a valid email.";
  if (input.validity.tooShort) return `Use at least ${input.minLength} characters.`;
  if (input.validity.patternMismatch) return input.title || "Invalid format.";
  return "Invalid value.";
}

setCustomValidity("…") adds a custom error; clear with setCustomValidity("") when fixed.

CSS hooks

input:required {
  /* subtle cue */
}

input:invalid {
  /* avoid yelling on empty pristine fields — use :user-invalid where supported */
  border-color: var(--danger);
}

input:user-invalid {
  border-color: var(--danger);
}

:user-invalid / :user-valid reduce “red borders on page load” noise.

Server validation still required

Client attributes are UX, not security. Re-validate on the server every time. Attackers don’t use your HTML.

Accessible error messages

  1. Associate messages with aria-describedby or aria-errormessage (support varies).
  2. Set aria-invalid="true" when showing errors.
  3. Move focus to the first invalid field on submit.
  4. Don’t rely only on color.

Interview out-loud

“Native constraint validation covers required, types, ranges, lengths, and patterns. I use checkValidity/validity states for custom messages, prefer :user-invalid styling, and always re-validate on the server. Accessible errors need text, associations, and focus management—not just red borders.”

Footguns

  1. pattern without a human title/custom message.
  2. type="number" still allows e/+ quirks — know the validity model.
  3. Disabling validation with novalidate and forgetting to reimplement it.
  4. Validating only on submit with no field-level feedback.
  5. Trusting client-only checks for auth or prices.

setCustomValidity async example

const username = document.querySelector("#username");
username.addEventListener("blur", async () => {
  username.setCustomValidity("");
  if (!username.validity.valid) return;
  const taken = await fetch(`/api/username-taken?u=${encodeURIComponent(username.value)}`)
    .then((r) => r.json())
    .then((j) => j.taken);
  if (taken) username.setCustomValidity("That username is taken.");
});

Clear custom validity before rechecking. Combine with reportValidity() on submit for UA bubbles, or suppress bubbles with novalidate and your own error UI that reuses validity flags.

Further reading

Related guides