< form >
The form element: more capable than you may remember
A native data-collection and submission element. HTML gives you submission handling, built-in client-side validation, constraint styling hooks, the full HTTP method and encoding surface, and structured data extraction, all before any JavaScript runs. Form-related features have been among the most actively developed areas of the HTML specification in recent years, which means there is quite a bit here that predates most current training materials.
Built-in with native HTML
- Submission. A
<form>with anactionand amethodsubmits to the server without a single line of JavaScript. Ifactionis omitted, the form submits to the current URL, which is often exactly what you want. - Constraint validation. The attributes
required,minlength,maxlength,min,max,pattern, andstep, along with the input type itself (email,url,number,tel), all participate in built-in validation. The browser blocks submission, focuses the first invalid field, and shows a native message. - The
:valid,:invalid,:user-valid,:user-invalid,:required,:optional,:in-range, and:out-of-rangepseudo-classes. These let CSS respond to form state without any JavaScript. The:user-validand:user-invalidpair specifically only activates after the user interacts, so fields don't light up red on page load. FormDataconstruction. Passing a form element tonew FormData(form)returns a structured, iterable object matching every named input, including files. This is the fastest way to extract form values, and it handles file inputs, checkboxes, radio groups, and multi-selects correctly. Custom value-gathering code often misses one of these cases.- Reset. A
<button type="reset">restores every input to its initial value. No state snapshot, no saved-values object. The form element remembers.
The submission model, in full
A form submission is defined by four attributes on the <form> element, each of which can be overridden by the submit button that triggered the submission. See the button element page for formaction, formmethod, formenctype, and formnovalidate.
actionis the URL to submit to. Defaults to the current page.methodisGETorPOST. A third value,DIALOG, is covered on the dialog element page.enctypeis how the data is encoded. Three values:application/x-www-form-urlencoded(default)multipart/form-data(required for file uploads)text/plain(rarely useful)
targetis where the response loads._selfby default._blankfor a new tab. A named iframe for an in-page response.
The combination of method="GET" and a thoughtful form is how search boxes, filter forms, and pagination are meant to work. The URL becomes the state, which means bookmarking, sharing, and back-button navigation all work without anything added.
<form method="get" action="/search">
<label>Search <input type="search" name="q"></label>
<label>Category
<select name="category">
<option value="">All</option>
<option value="books">Books</option>
</select>
</label>
<button>Search</button>
</form>
That form produces a URL like /search?q=quilting&category=books. It is bookmarkable, shareable, and the back button restores the exact query. No router needed.
Intercepting submission without breaking the form
The pattern for progressive-enhancement submission, where the form works without JavaScript and the JavaScript layer makes it faster or smoother, is to listen for the submit event and call event.preventDefault():
form.addEventListener('submit', async (event) => {
event.preventDefault();
const data = new FormData(form);
const response = await fetch(form.action, {
method: form.method,
body: data,
});
// handle response
});
This is worth contrasting with the more common pattern of gathering values from refs or controlled inputs. The native form element already knows the values. FormData(form) reads them all. A form that works with and without JavaScript is a form that still works when a script fails to load, when the user is on a slow connection and submits before hydration, and when the network blips mid-request.
The form-associated attributes on inputs
Any input, button, select, or textarea can reference a form elsewhere on the page using the form attribute:
<form id="checkout">...</form>
<input type="text" name="coupon" form="checkout">
<button form="checkout">Apply</button>
This means form inputs do not have to be descendants of the form element. A sticky footer submit button that lives at the bottom of the page while the form lives in the main content area is a single attribute, not a portal.
Constraint validation API
Every form and every form control exposes the same validation interface:
element.checkValidity()returns true or false.element.reportValidity()checks validity and, if invalid, focuses the field and shows the native message.element.setCustomValidity(message)sets a custom error message. An empty string clears it. The field then reports as invalid until cleared.element.validityis aValidityStateobject with flags likevalueMissing,typeMismatch,patternMismatch,tooShort,tooLong,rangeUnderflow,rangeOverflow,stepMismatch,badInput, andcustomError.form.elementsis a live collection of all form controls, indexable by name or by integer.
The invalid event fires on a field when submission is attempted and the field is invalid. It does not bubble, so listen on the field itself or use capture.
The ValidityState object is how a form library is built. If one is being built in-house, this is the API to wrap, not replace.
Custom error messages, localized
Native validation messages are localized to the user's browser locale. If custom messages are needed, like "Passwords must match", setCustomValidity sets the string that the browser will show in its native bubble using the browser's native styling. Custom validation messages then look native, appear in the right place, and work with assistive technology, without building a tooltip system.
Accessibility: the form as a landmark
A <form> element with an accessible name, through aria-label or aria-labelledby, is exposed as a navigation landmark. Screen reader users can jump between landmarks. For sites with multiple forms on a page, such as a search form, a newsletter signup, and a contact form, naming them turns each into a navigable region.
<form aria-labelledby="search-heading">
<h2 id="search-heading">Search</h2>
...
</form>
Fieldset and legend: grouping
The <fieldset> and <legend> elements group related controls and announce the group name as part of each control's accessible name inside. For a "Shipping address" block with five fields, wrapping in a fieldset means each field is announced as "Street address, edit, within Shipping address" by screen readers. That context would otherwise need to be repeated in every label.
Fieldset also has a disabled attribute that disables every descendant form control in one place:
<fieldset disabled>
<legend>Shipping address</legend>
<!-- every input here is disabled -->
</fieldset>
Useful while a section is loading, or conditionally based on other form state.
Composite patterns that use <form> well
- Search.
<form method="get" action="/search">with an<input type="search">. The URL becomes the state. - Filter UIs. Same pattern as search. Check state is serialized in the query string.
- Multi-step wizards. Each step is a form that posts to an endpoint returning the next step, or one long form with fieldsets revealed progressively. Both work without JavaScript.
- Inline editing. Each editable item is a small form with its own action URL.
- Confirmation dialogs. A
<form method="dialog">inside a<dialog>element, covered on the dialog page. - Login, signup, password reset. The classic case where progressive enhancement matters most. These forms need to work on slow connections, older browsers, and every assistive technology.
Already HTML-available
Patterns that HTML handles natively, which are often rebuilt in JavaScript:
- Gathering values from refs or controlled state.
new FormData(form)returns them structured correctly, including files. - Custom reset logic. A
<button type="reset">resets every field to its initial value. - Manual validation state tracking. The
form.checkValidity()method and the:user-invalidpseudo-class surface validity to code and CSS. - Per-field "which field errored" logic. The
invalidevent fires on each invalid field with the reason available inelement.validity. - Wrapping form data in a custom object for submission.
FormDatais the shapefetchaccepts as a body. - Serializing form state to the URL manually. The
method="get"attribute does this as the default behavior.
Reference:
- WHATWG
- the-form-element
- form-submission algorithm
- caniuse.com
- mdn-html_elements_form
- MDN Docs
- Web/HTML/Element/form
- Client-side form validation
- FormData
- CSS: :user-valid
- CSS: :user-invalid