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>
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.
<!-- 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 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.
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.
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 programmaticallyif (input.checkValidity()) { // passes all constraints}// Inspect *why* it failedconsole.
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.
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.
<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.
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#
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.
<!-- 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.
<!-- 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).
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.
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.