"A form is a contract. Every field is a question, every label is the question text, every input is the user's answer."
The <form> Element Itself
The <form> wraps a group of inputs that submit together. The attributes that matter:
action="/api/signup"— where the form data is sent.method="post"— almost alwayspostfor forms that change state;getonly for search-style forms that read.autocomplete="on" | "off"— controls browser autofill. Default is on. Turning it off site-wide is a hostile-to-users move; turn off per-field only when there's a real reason (a one-time PIN, an OTP).novalidate— disables the browser's built-in HTML5 validation. Useful when you're doing custom validation but want browsers to still send the form on submit.
<label> + <input>: The Atomic Pair
This is the most important pattern in the entire track. Every input needs a label. Two ways to associate them:
- Explicit (preferred):
<label for="email">Email</label> <input id="email" type="email" />. Theforattribute on label matches theidon input. - Implicit:
<label>Email <input type="email" /></label>. Wrap the label around the input. No id required.
Both give the input an accessible name screen readers can announce, and both make the label text clickable (which focuses or toggles the input). Skip the label and your form fails WCAG, fails Lighthouse, and ships an input that screen readers describe as "edit text" with no context.
The name Attribute
The name attribute is what your backend (or JavaScript) receives. <input name="email" /> means the form submission sends email=user@example.com. Without name, the input is essentially invisible to form submission — it has a value but nothing is sent. Common bug: building a form, wondering why the backend sees nothing, realizing no inputs have names.
<fieldset> + <legend>: Grouping Related Fields
When several inputs belong together — a billing address (street, city, zip), a radio group ("Choose a plan"), a checkbox group ("Email me about...") — wrap them in <fieldset> with a <legend> that names the group. Screen readers announce "Billing address, group, 5 items" before reading the fields.
Radio buttons are the canonical case: they only make sense as a group. The name attribute ties them together (only one can be checked); the <fieldset> + <legend> tells the user what the group represents.
State Attributes: required, disabled, readonly
required— the form will not submit without it. The browser shows a built-in validation message in the user's language.disabled— input is unfocusable, ungreyed visually, not submitted with the form. Use for fields that aren't applicable to the current flow.readonly— input shows the value but the user can't edit. Is submitted with the form. Use for confirmation fields where the value was computed.
Subtle distinction: disabled excludes from submission, readonly includes it. Pick on the basis of what you want the server to see.
<button>: type Attribute Matters
type on <button>. The default is type="submit", which means any <button> inside a form will submit the form when clicked. If you have a 'Reset password' button next to a 'Sign up' button, both will submit unless you set type="button" on the non-submit ones. This catches every developer at least once.The three types:
type="submit"— submits the parent form (default).type="reset"— clears all form fields back to their initial values. Almost never wanted; one accidental click destroys user data.type="button"— does nothing by default. For buttons that run JavaScript (toggle visibility, open a modal).
Native Validation: Free For The Asking
HTML5 validation runs in the browser without any JavaScript: required, type="email", type="url", pattern="...", min, max, minlength, maxlength. Submit a form with invalid data → the browser shows a localized error message and focuses the first invalid field. Style invalid fields with :invalid in CSS. Catch the error event with noValidate + custom UI when you outgrow it. Don't reinvent the wheel until the wheel can't get you there.
Pippa's Note
required, type="email", minlength, pattern, and a tiny bit of :invalid CSS. The error messages come from the browser in the user's locale (Korean shows Korean errors, English shows English). Less code, more correct behavior, and translations for free.