What <form> does
<form> groups input controls and gives them a submission target.
The element accepts:
action— the URL to submit to.method—get(default) orpost. Note thatpostis required formultipart/form-data.enctype—application/x-www-form-urlencoded(default),multipart/form-data(file uploads), ortext/plain(rare).target— same as<a target>; rarely useful in modern UIs.novalidate— opt out of native constraint validation.
A submit-type button or <input type="submit"> inside the form
triggers submission. The Enter key in any single-line text input also
triggers submission unless preventDefault is called.
The submission lifecycle
When a submit attempt occurs, the user agent fires events in this order:
formdataon the form — the new event, where authors can mutate theFormDatabefore the request is sent.- Constraint validation — built-in checks against
required,pattern,min,max,step,minlength,maxlength, etc. invalidon each invalid control, thensubmiton the form. IfsubmitispreventDefault()-ed, the request is suppressed.- Network submission if not prevented.
Calling form.requestSubmit() exercises this full pipeline; calling
form.submit() skips validation and submit events entirely. Use
requestSubmit unless you have a specific reason.
Native constraint validation
The constraint API exposes:
input.validity— aValidityStatewith eight boolean properties.input.validationMessage— a localised error string from the UA.input.setCustomValidity(msg)— set a custom message; call with""to clear.input.checkValidity()andinput.reportValidity()— programmatic triggers; the latter shows the UA bubble.
The UA-supplied message is localised to the user’s language. Custom messages override that localisation, so prefer the native message when possible and only set custom for app-specific rules.
FormData and serialisation
new FormData(form) produces a FormData instance containing the
current values of every named control inside the form. Use this to
build a Fetch request body:
form.addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
const res = await fetch(form.action, {
method: form.method.toUpperCase(),
body: data,
});
// ...
});
FormData correctly handles file uploads, multi-value selects, and
checkbox arrays. Avoid round-tripping through plain objects unless
you also reimplement those semantics.
Accessibility
Every control needs an accessible name. The order of precedence for form controls is:
<label for="...">referencing the control by id (preferred).- A wrapping
<label>. aria-labelledby.aria-label.placeholder(last resort, not equivalent to a label).
Group related controls with <fieldset> and <legend> — the
<legend> is announced as a prefix to each control inside.
Browser engine support
Constraint validation, FormData, the formdata event, and
requestSubmit reached cross-engine support across Chromium (Blink),
WebKit, and Gecko by early 2022. The inert attribute on form
elements (used to disable a fieldset under load) reached interop in
2023.
Common pitfalls
form.submit()instead ofform.requestSubmit(). Bypasses validation and confuses users when a hidden constraint fails on the server.<button>withouttypeinside a form. Defaults tosubmit, notbutton. Addtype="button"for non-submitting buttons.- Custom error messages without
setCustomValidity('')reset. The error sticks across attempts; users see stale text. - Async validation in the
submithandler. UA validation runs synchronously before the event; an async backend check should happen aftersubmit(withpreventDefault) and surface results viasetCustomValidityandreportValidity.
Further reading
- HTML Living Standard, §4.10 Forms.
- The
formdataevent landed in HTML in 2019 and reached interop in 2020 (wpt.fyi formdata). - The Constraint Validation Living Spec for the precise ValidityState semantics.
- Chris Coyier’s form attributes cheatsheet is dated 2021 but remains the densest single-page reference.
- Web Platform Tests for forms hover around 95% pass-rate across Chromium, WebKit, and Gecko per the 2024 Interop dashboard.