06 · Forms & Native Validation¶
Forms are where a page stops being a document and starts being an application — sign-in, search, checkout, contact, settings. HTML's form elements are remarkably capable on their own: they handle keyboard input, mobile keyboards, autofill, validation and submission without a line of JavaScript. Most broken forms on the web are broken because someone replaced those built-ins with something custom and didn't rebuild all of it.
Anatomy of a form¶
<form action="/signup" method="post">
<p>
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required>
</p>
<p>
<label for="password">Password</label>
<input id="password" name="password" type="password"
autocomplete="new-password" minlength="12" required
aria-describedby="password-hint">
<span id="password-hint">At least 12 characters.</span>
</p>
<fieldset>
<legend>Plan</legend>
<label><input type="radio" name="plan" value="free" checked> Free</label>
<label><input type="radio" name="plan" value="pro"> Pro</label>
</fieldset>
<p>
<label>
<input type="checkbox" name="newsletter" value="yes">
Send me the monthly newsletter
</label>
</p>
<button type="submit">Create account</button>
</form>
action— where to send the data. Omit it to submit to the current URL.method—get(the default) puts the data in the URL query string; use it for searches and filters, which should be bookmarkable.postsends it in the request body; use it for anything that changes data or includes secrets.name— the key each value is submitted under. A field without anameis not submitted.<label>— covered next; the single most important form element.<button type="submit">— submits. Pressing Enter in a text field does too.
Labels¶
Every form control needs a label. Two ways to associate one:
<!-- explicit: for= matches the input's id -->
<label for="city">City</label>
<input id="city" name="city">
<!-- implicit: the input is inside the label -->
<label>City <input name="city"></label>
A label does three jobs: it's the control's accessible name (what a screen reader announces), it's a bigger click/tap target (clicking the label focuses the input or toggles the checkbox), and it's visible instructions for everyone.
placeholder is not a label. It disappears as soon as you type, is usually low
contrast, and isn't reliably announced. Use it, if at all, for an example format
("e.g. 560001").
Input types¶
The type attribute changes validation, the on-screen keyboard on phones and the
built-in UI:
| Type | Use for | What you get |
|---|---|---|
text |
Anything free-form | Default |
email |
Email addresses | Format validation, @ keyboard |
tel |
Phone numbers | Numeric phone keypad; no format validation (formats vary worldwide) |
url |
Web addresses | URL validation |
number |
Quantities you'd increment | Spin buttons, min/max/step validation |
password |
Secrets | Masked input |
search |
Search boxes | Clear button in some browsers |
date, time, datetime-local, month, week |
Dates and times | Native pickers |
checkbox |
Independent yes/no | |
radio |
One choice from a group (same name) |
Arrow keys move within the group |
range |
Approximate value on a slider | |
color |
Colour picker | |
file |
Uploads (accept=".pdf,image/*", multiple) |
Needs method="post" and enctype="multipart/form-data" |
hidden |
Values the user doesn't edit |
type="number" is for quantities, not for anything that merely contains digits —
postcodes, card numbers and OTP codes should be type="text" with
inputmode="numeric" (which shows a number keypad without spin buttons or number
parsing that strips leading zeros).
Other controls¶
<label for="size">Size</label>
<select id="size" name="size">
<option value="">Choose a size</option>
<optgroup label="Soups">
<option value="s">Small (250 ml)</option>
<option value="m" selected>Medium (400 ml)</option>
</optgroup>
</select>
<label for="notes">Delivery notes</label>
<textarea id="notes" name="notes" rows="4"></textarea>
Grouping with fieldset and legend¶
Radio buttons and related checkboxes need a group label: "Free" and "Pro" mean nothing
without "Plan". <fieldset> groups them and <legend> names the group; screen readers
announce "Plan, group" on entering it. <fieldset disabled> disables every control
inside at once.
Worked example: what actually gets submitted¶
Submitted data is a list of name/value pairs, built by a precise algorithm. We built
this form in Chromium and read it with new FormData(form):
<form id="f">
<input name="email" type="email" value="ana@example.com">
<input value="no-name">
<input name="plan" type="radio" value="free">
<input name="plan" type="radio" value="pro" checked>
<input name="news" type="checkbox">
<input name="terms" type="checkbox" checked>
<input name="tags" type="checkbox" value="css" checked>
<input name="tags" type="checkbox" value="html" checked>
<input name="token" value="abc" disabled>
<input name="uid" value="42" readonly>
<select name="size"><option>S</option><option selected>M</option></select>
<textarea name="notes">hi</textarea>
<button name="action" value="save">Save</button>
</form>
FormData: [["email","ana@example.com"],["plan","pro"],["terms","on"],["tags","css"],
["tags","html"],["uid","42"],["size","M"],["notes","hi"]]
URL-encoded: email=ana%40example.com&plan=pro&terms=on&tags=css&tags=html&uid=42&size=M¬es=hi
Every rule in that output is one you'll trip over eventually:
<input value="no-name">— noname, not submitted.news— an unchecked checkbox sends nothing at all, notnews=off. The server has to treat "missing" as false.terms— a checked checkbox without avaluesendson.tagsappears twice. Multiple values under one name is normal; your server code must expect a list.token— disabled fields are not submitted.uidisreadonlyand is submitted. Usereadonlyfor "show but don't edit".size=M— an<option>withoutvaluesubmits its text.action=saveis absent because we constructedFormDatadirectly; the button's name/value is only included when that button is what submitted the form. That's how "Save" and "Publish" buttons in one form are told apart.
With method="get", the URL-encoded string becomes the query string:
/signup?email=ana%40example.com&plan=pro....
Native constraint validation¶
Attributes declare the rules; the browser enforces them on submit, shows a message, and focuses the first invalid field:
| Attribute | Rule |
|---|---|
required |
Must have a value (for radios: one in the group checked) |
type="email" / "url" |
Must match the format |
minlength, maxlength |
Text length |
min, max, step |
Numbers and dates |
pattern |
A regular expression the whole value must match |
We checked four fields in Chromium via each element's validity object and
validationMessage:
type=email required value="ana@" -> typeMismatch "Please enter a part following '@'. 'ana@' is incomplete."
type=number min=1 max=10 value="12" -> rangeOverflow "Value must be less than or equal to 10."
pattern="[A-Z]{3}-[0-9]{3}" "ab-123" -> patternMismatch "Please match the requested format."
required, empty -> valueMissing "Please fill out this field."
The messages are the browser's own and vary by browser and language. The generic
"Please match the requested format" is why you should always describe the expected format
in visible text next to a pattern field.
Styling valid and invalid states¶
Use :user-invalid rather than :invalid. :invalid matches immediately — every empty
required field is red before the user has typed anything. :user-invalid only matches
after the user has interacted with the field or tried to submit.
Client-side validation is a convenience, not security¶
Anyone can bypass it — by editing the HTML in devtools, adding novalidate, or sending
a request without a browser at all. Always validate again on the server. Client-side
validation exists to give fast feedback, not to protect your data.
How It Actually Works¶
On submission the browser runs the form submission algorithm:
- Fire the
submitevent (scripts can cancel it). - Unless the form has
novalidateor the submitter hasformnovalidate, run static constraint validation: check each control'svalidity. If any fail, fireinvalidevents, report the first problem to the user, and stop. - Construct the entry list: walk the form's controls in tree order, skip those that are disabled, have no name, are unchecked checkboxes/radios, or are buttons other than the submitter; add name/value pairs for the rest.
- Encode the list using the form's
enctype—application/x-www-form-urlencoded(the default:a=1&b=2with percent-encoding),multipart/form-data(needed for files), ortext/plain(don't use it). - Navigate to the
actionURL with that data, usingmethod.
Controls belong to a form by being inside it — or, for controls elsewhere on the page, by
a form="form-id" attribute. That's occasionally useful for layouts where a submit
button must live outside the <form> element.
Common mistakes¶
- Missing
name, so the field silently never submits. - Placeholder instead of label.
- Radios without
fieldset/legend, or radios with differentnames (then they aren't a group and all can be checked). type="number"for codes and IDs — leading zeros vanish, and scroll wheels change the value.- Buttons without
type. Inside a form, a<button>defaults totype="submit", so a "Show password" button submits the form. Writetype="button"for non-submit buttons. - Relying on client-side validation for security.
- Turning off autofill with
autocomplete="off"on login fields. Use correctautocompletetokens instead (email,current-password,new-password,postal-code,one-time-code) so password managers and autofill work.
Exercise¶
- Build a "Book a cooking class" form with: name, email, phone, a date, a class choice
(radios in a fieldset), dietary requirements (checkboxes), a notes textarea and a
submit button. Give every field a label, a
nameand a sensibleautocompletetoken. - Set
method="get"and noaction. Submit it and read the URL — confirm every rule from the worked example (unchecked boxes missing, repeated names, etc.). - Add
required,minlengthand amindate. Try submitting empty, then partly filled. - Add
input:user-invalidstyling and compare it withinput:invalid. - In devtools, delete the
requiredattribute from a field and submit. What does this tell you about server-side validation?