DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/HTML/Forms & Validation
beginner20 min read·Updated Jul 2026

Forms & Validation

Native HTML forms handle most validation, keyboard interaction, and accessibility for free — most engineers reinvent it badly in JavaScript instead. This topic covers input types, native validation, labeling, and the associated JS APIs.

htmlformsvalidationaccessibilityinputs

Knowledge Check

4 questions · pass at 70%

0/4
easymcq

1. What is the primary accessibility problem with using `placeholder` instead of `<label>`?


Interview Questions

3 questions

Cheatsheet

Download-ready reference
Cheatsheet
Input types → what you get for free:
  email      @ and .com mobile keyboard shortcut + basic shape validation
  tel        numeric keypad on mobile
  number     spinner UI + numeric keypad + min/max/step validation
  url        URL-shape validation + / and .com mobile keyboard
  date       native locale-aware date picker
  search     clear (×) button on most browsers
  password   masked input + password manager integration

Native validation attributes:
  required            field must have a value
  minlength / maxlength   string length bounds
  min / max / step    numeric/date bounds
  pattern="regex"     custom regex constraint (pair with `title` for the error message)

Constraint Validation API (JS):
  input.checkValidity()      → boolean, triggers 'invalid' event if false
  input.reportValidity()     → like checkValidity() but also shows the native bubble
  input.validity             → ValidityState object (valueMissing, typeMismatch, tooShort, ...)
  input.validationMessage    → human-readable current error string
  input.setCustomValidity(msg)  → set (or clear with "") a custom error

Form submission:
  new FormData(formElement)  → collects every *named* field automatically, incl. files
  e.preventDefault() in 'submit' handler → stop native navigation for fetch()-based submit

Labeling:
  <label for="id">...</label> + <input id="id">   → explicit association
  <label>...<input></label>                          → implicit association
  aria-label="..."                                    → fallback only, no visible label case

Grouping:
  <fieldset><legend>Question</legend>...radios/checkboxes...</fieldset>

Related topics

Semantic HTMLAccessibility (ARIA)Closures

On this page

20m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

Forms are the most interactive part of most web apps, and HTML gives you an enormous amount of behavior for free — input types, native validation, keyboard handling — before a single line of JavaScript. The most common mistake senior-looking codebases still make is rebuilding all of that in JS because nobody on the team learned what the platform already does.

Every input needs a <label>#

<!-- Wrong: placeholder is not a label -->
<input type="email" placeholder="Email address" />
 
<!-- Right: explicit label, associated by id -->
<label for="email">Email address</label>
<input id="email" type="email" name="email" />
 
<!-- Also valid: implicit association by wrapping -->
<label>
  Email address
  <input type="email" name="email" />
</label>

placeholder text disappears the moment the user starts typing, often has poor contrast, and isn't reliably read as a label by all assistive technology. A <label> is: always visible, clickable (clicking it focuses/activates the input), and is what screen readers announce as the field's accessible name.

Input types are not cosmetic#

<input type="email" />    <!-- mobile keyboards show @ and .com shortcuts -->
<input type="tel" />      <!-- numeric keypad on mobile -->
<input type="number" />   <!-- spinner controls, numeric keypad, range constraints -->
<input type="url" />      <!-- validates URL shape, mobile keyboard has / and .com -->
<input type="date" />     <!-- native date picker, locale-aware -->
<

Each type changes the mobile keyboard layout, triggers built-in validation, and can enable browser autofill and password manager suggestions. Using type="text" for everything throws away all of it.

Native validation attributes#

<input type="email" required />
<input type="text" minlength="3" maxlength="20" required />
<input type="number" min="1" max="100" step="1" />
<input type="text"

These constraints run automatically when the form is submitted — no JS required. The browser blocks submission and shows a native validation bubble pointing at the offending field. title supplies the message shown for pattern mismatches.

The Constraint Validation API#

For custom validation UI (native bubbles are functional but not very stylable), use the JS API rather than reimplementing validation logic from scratch:

const input = document.querySelector("#email");
 
input.addEventListener("invalid", (e) => {
  e.preventDefault(); // suppress the native bubble
  showCustomError(input.validationMessage);
});
 
// Check validity programmatically
if (input.checkValidity()) {
  // passes all constraints
}
 
// Inspect *why* it failed
console.



input.validity is a ValidityState object — every property is a boolean telling you exactly which constraint failed, which is far more precise than parsing a regex yourself.

Form submission without a page reload#

<form id="signup">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required />
  <button type="submit">Sign up</button>
</form>
document.querySelector("#signup").addEventListener("submit", async (e) => {
  e.preventDefault(); // stop the native full-page GET/POST navigation
  const formData = new FormData(e.target);
  await fetch("/api/signup", { method: "POST", body: formData });
});

FormData automatically collects every named field, including file inputs, without manually reading each .value. This is also what makes <form> (rather than a <div> of inputs with a JS click handler) valuable even in a fully JS-driven app: you get Enter-to-submit for free, and FormData for free.

Grouping related fields: <fieldset> and <legend>#

<fieldset>
  <legend>Shipping address</legend>
  <label for="street">Street</label>
  <input id="street" name="street" />
  <label for="city">City</label>
  <input id="city" name






<legend> is announced by screen readers before every field inside the group — critical for radio button groups where "Card" and "PayPal" alone are meaningless without knowing the question being answered.


Common Mistakes#

1. placeholder instead of <label>#

Covered above — the single most common forms accessibility bug. Test: if you can't tell what a field is for once it has text in it and you've scrolled the label out of view, it's broken.

2. Disabling the submit button until the form is "valid" via JS, duplicating native validation#

// Wrong: reimplementing what `required` + `type="email"` already do
button.disabled = !email.includes("@") || password.length < 8;

This blocks keyboard submission (Enter), doesn't tell the user why the button is disabled, and duplicates logic the browser already runs. Prefer letting the form submit and be blocked/messaged by native (or Constraint Validation API) validation.

3. Using <div> + onclick for radio/checkbox groups#

Custom-styled "toggle" UIs frequently throw away the actual <input type="radio">/<checkbox> and rebuild toggling in JS with <div>s. This loses native form submission (FormData won't include it), native keyboard behavior (arrow keys move between radios in a group automatically), and screen reader semantics. Style the real input (opacity: 0 + a sibling visual element, or the appearance CSS property) instead of replacing it.

4. Forgetting name attributes#

<!-- Wrong: has an id but no name -->
<input id="email" type="email" />

FormData and traditional form submission both key off name, not id. A field with only an id silently vanishes from submitted data.

5. Client-side-only validation with no server-side check#

Native/JS validation is a UX layer, not a security boundary — it runs in the browser and can be trivially bypassed (disable JS, or fetch() directly). Every constraint enforced in the form must be re-validated on the server.

6. Autocomplete attribute neglect#

<!-- Missing autocomplete means browsers/password managers have to guess -->
<input type="text" name="cc-number" />
 
<!-- Right -->
<input type="text" name="cc-number" autocomplete="cc-number" />

autocomplete hints (email, name, given-name, street-address, cc-number, one-time-code, etc.) are what let browsers and password managers autofill correctly and are an accessibility requirement under WCAG 2.1 (Success Criterion 1.3.5).


Best Practices#

  • Every input gets a real <label>, associated via for/id or by wrapping. aria-label is a fallback for when no visible label is appropriate (e.g. an icon-only search field), not a first choice.
  • Choose the most specific type for every input — it's free mobile keyboard optimization and free basic validation.
  • Use native constraint attributes (required, min/max, pattern, minlength) before reaching for JS validation; layer the Constraint Validation API on top only for custom error UI.

Further Resources#

  • MDN — Forms guide
  • MDN — Constraint Validation API
  • MDN — autocomplete attribute values
  • web.dev — Sign-in form best practices
  • web.dev — Payment and address form best practices

input
type
=
"search"
/>
<!-- adds a clear (×) button on most browsers -->
<input type="password" /> <!-- masks input, offers password manager integration -->
pattern
=
"[A-Za-z]{3,}"
title
=
"At least 3 letters"
/>
log
(input.validity);
// ValidityState { valueMissing, typeMismatch, patternMismatch, tooShort, tooLong, rangeUnderflow, rangeOverflow, stepMismatch, ... }
// Set a custom error message (e.g. from an async check)
input.setCustomValidity(isUsernameTaken ? "Username is already taken" : "");
=
"city"
/>
</fieldset>
<fieldset>
<legend>Payment method</legend>
<label><input type="radio" name="pay" value="card" /> Card</label>
<label><input type="radio" name="pay" value="paypal" /> PayPal</label>
</fieldset>
  • Always validate on the server too. Client-side validation is UX, not security.
  • Add autocomplete attributes — small effort, meaningfully better UX and an accessibility win.
  • Group related fields with <fieldset>/<legend>, especially radio/checkbox groups.
  • Don't disable the submit button speculatively. Let users attempt submission and see specific errors — a disabled button with no explanation is a common source of "why can't I submit this form" support tickets.
  • WHATWG HTML spec — Forms