Fixing Form Markup Errors
Forms produce a distinctive set of validator errors: a label whose for attribute points at nothing, maxlength on a number field, an autocomplete value the browser does not recognise, a form nested inside another form, a required select with no empty first option.
Each one changes how the form behaves, from autofill that never fills to fields that are silently left out of the submission. This guide works through the validator messages for form markup, explains what each one does to the form, and gives corrected markup. For writing good visible labels and error messages, see the separate form labels guide.
- What this guide covers, and what it does not
- The validator messages for forms, and what each means
- Structural errors: nested forms and misplaced controls
- Attributes that each input type allows
- Autocomplete, inputmode and boolean attributes
- The fix, step by step
- Where these errors come from on common platforms
- Questions and answers
What this guide covers, and what it does not
There are two quite different questions you can ask about a form. One is whether a person can understand and complete it: are the labels clear, are errors explained, can it be used by keyboard and screen reader? That is covered in fixing form labels. The other is whether the markup itself is valid HTML, which is what the HTML Checker reports and what this guide fixes.
The two overlap, because invalid markup often breaks accessibility. A label pointing at an id that does not exist, for instance, is both a validation error and an unlabelled field. But many form errors have nothing to do with labels: they are attributes that do not belong on a given input type, values the browser cannot interpret, or structure the HTML parser rewrites before your page even loads.
A note on the word "validation": HTML also has built-in constraint validation, the browser's
own checking of required, pattern, min and type="email"
fields before submission. Invalid markup is one of the main reasons that checking behaves oddly, so fixing
the markup is also the first step when built-in form checking seems broken.
The validator messages for forms, and what each means
| Message (shortened) | What it means | Effect on the form |
|---|---|---|
| The for attribute of the label element must refer to a non-hidden form control | The label's
for value matches no id, or matches a hidden input or a non-form element | The field has no programmatic label; clicking the label does nothing |
| Any input descendant of a label element with a for attribute must have an ID value that matches that for attribute | A field is wrapped in a label whose for names a different id | Conflicting associations; the label may attach to another field |
| The label element may contain at most one button, input, meter, output, progress, select, or textarea descendant | Two fields inside one label, such as day and month selects | Only one of the fields is labelled |
| Attribute maxlength not allowed on element input at this point | maxlength
used on type="number", or another attribute that the type does not support | The attribute is ignored, so the limit you expected is not enforced |
| Bad value "x" for attribute type on element input | An input type that does not exist,
such as datetime or phone | The browser falls back to a plain text box |
| Bad value "nope" for attribute autocomplete on element input | A made-up autocomplete value | Autofill hints are lost |
| Start tag form seen but an element of the same type was already open | A <form>
inside another form | The inner form tag is discarded; its fields submit with the outer form |
| Bad value "" for attribute action on element form: Must be non-empty | action="" | Works by accident (submits to the current URL) but is invalid |
| The first child option element of a select element with a required attribute⦠must have either an empty value attribute, or must have no text content | A required select whose first option is a real-looking value | The field is always "filled", so required never triggers |
| Bad value "false" for attribute required on element input | A boolean attribute given the value "false" | The field is still required |
| The element button must not appear as a descendant of the a element | A button wrapped in a link, or a link wrapped in a button | Unpredictable clicks and keyboard behaviour |
| The form attribute must refer to a form element | A control's form="β¦" points
at an id that is not a form | The control is not submitted with any form |
| Stray end tag input | A closing </input> | Harmless, but noisy; input is a void element |
The exact wording varies slightly between validator versions, but the key phrase, such as "not allowed on element input" or "must refer to", stays the same and is what you should search for.
Structural errors: nested forms and misplaced controls
Fix these first, because the HTML parser changes the document when it meets them, and the other errors may be side effects.
A form inside a form
Forms cannot be nested. When the parser sees a second <form> start tag while one
is open, it ignores it, and the inner form's action and method vanish. Its
fields become part of the outer form. This usually happens when a page builder puts a newsletter or search
widget inside a section that is already wrapped in a form, or when a checkout theme wraps the whole page
in one form.
Move the inner form outside the outer one. If the controls must sit visually inside the outer form,
place the second form elsewhere in the markup and link its controls to it with the form
attribute:
<form id="checkout" action="/checkout" method="post">
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="email" required>
<label for="voucher">Voucher code</label>
<input type="text" id="voucher" name="code" form="voucher-form">
<button type="submit" form="voucher-form">Apply</button>
<button type="submit">Place order</button>
</form>
<form id="voucher-form" action="/voucher" method="post"></form>
Buttons inside links, and links inside buttons
Interactive elements cannot contain other interactive elements. A "Shop now" button wrapped in a link
is really a navigation link, so use an <a> styled as a button. Something that submits,
opens or toggles is a <button>. Choose one; never nest them.
Buttons without an explicit type
Inside a form, a <button> with no type is a submit button. A "show
password" or "add another item" button without type="button" will submit the form when pressed.
The validator does not always flag a missing type, but the HTML Checker's form checks do, and it is worth
setting on every button: type="submit", type="button" or, rarely, type="reset".
Void elements and closing tags
input is a void element: it has no content and no closing tag. Delete any </input>.
By contrast textarea, select and button must be closed; a missing
</textarea> swallows the rest of the page into the text box.
Attributes that each input type allows
Most attribute errors come from treating every input as a text box. Each type accepts only the attributes that make sense for it, and anything else is ignored by the browser, which means the behaviour you expected never happens.
| Attribute | Allowed on | Common mistake |
|---|---|---|
maxlength, minlength, pattern, placeholder,
size | text, search, url, tel, email, password (placeholder also on number) | maxlength
on a number field to limit digits |
min, max, step | number, range, date, month, week, time, datetime-local | min on a text field |
multiple | email, file (and select) | multiple on a text field |
accept | file | On an image upload button that is not a file input |
checked | checkbox, radio | On a select option (use selected) |
required | Every type except hidden, range, color and the button types | On a hidden input |
readonly | Text-like and date types | On a checkbox or select, where
it has no effect (use disabled, and remember disabled fields are not submitted) |
Limiting digits in a number field
Since maxlength does nothing on type="number", use min and
max for a numeric range. For codes that are made of digits but are not quantities, such
as a card security code, a one-time passcode or a phone extension, use a text input with inputmode="numeric",
which shows a numeric keypad on phones, plus pattern and maxlength:
<label for="otp">6-digit code</label>
<input type="text" id="otp" name="otp" inputmode="numeric"
pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code">
Phone numbers belong in type="tel", never type="number": a number field
drops leading zeros in some cases, rejects spaces and plus signs, and adds spinner arrows.
Types that do not exist
type="datetime" was removed from HTML; use datetime-local for a date and
time without a time zone. type="phone" and type="postcode" were never valid;
use tel and text with a suitable autocomplete value.
Autocomplete, inputmode and boolean attributes
Valid autocomplete values
autocomplete takes a defined list of tokens. Common ones are name, given-name,
family-name, email, tel, organization, street-address,
address-line1, address-level2 (town or city), postal-code, country-name,
username, current-password, new-password, one-time-code
and the card tokens such as cc-number. You can prefix a section name and shipping
or billing, as in autocomplete="shipping postal-code". Plain on
and off are also valid.
Invented values such as nope, false, disabled or new-field
are invalid. They became popular as tricks to stop autofill, and browsers handle them inconsistently.
If a field genuinely should not be filled from saved data, off is the valid way to say so;
for a password-change form, new-password tells password managers to suggest a new password
rather than fill the old one.
inputmode values
inputmode only accepts none, text, decimal, numeric,
tel, search, email and url. Values such as number
or digits are invalid and ignored.
Boolean attributes
required, disabled, checked, selected, readonly,
multiple, autofocus and novalidate are boolean: their presence
means true. required="false" still makes the field required, and checked="false"
still ticks the box. Valid forms are the bare attribute or the attribute name as its value, required="required".
To make something not required, leave the attribute out. Template code that prints disabled="{{ is_disabled }}"
is a frequent source of this bug.
Required selects need an empty first option
<label for="size">Size</label>
<select id="size" name="size" required>
<option value="">Choose a size</option>
<option value="s">Small</option>
<option value="m">Medium</option>
</select>
Without the empty-value first option, the select always has a value, so required can
never fail and visitors who never touch the field submit whatever the first option happens to be.
The fix, step by step
Step 1
Validate every page that has a form
Forms live on more pages than people expect: contact, quote request, newsletter sign-up in the footer, search in the header, login, account, basket and checkout. Run the HTML Checker on one page of each kind, including any with a form loaded in a modal. Copy the form-related messages into a list, noting the field id or name each one mentions, so you can find the exact line in the template afterwards.
Step 2
Fix structural errors first
Deal
with nested forms, buttons inside links, missing closing tags on textarea or select,
and stray </input> tags before anything else. The parser rewrites the document around
these, so they can create other errors further down the form. Move any inner form out and connect its
controls with the form attribute, then validate again. Several later errors often disappear
once the structure is sound.
Step 3
Repair label and form associations
For
every error mentioning a for or form attribute, check that the value exactly
matches the id of the intended control, including case. Make sure that control is not a hidden input,
that only one field sits inside each label, and that the id is unique on the page. Where a field has
been renamed in the template but its label has not, update both together from a single variable.
Step 4
Match attributes to each input type
For each "not allowed on element input" message, compare the attribute with the input's type
using the table above. Remove attributes the type ignores, or change the type if the wrong one was chosen:
tel for phone numbers, a text input with inputmode="numeric" for codes, and
min and max instead of maxlength for real quantities. Replace
any invalid type such as datetime with a valid one.
Step 5
Correct autocomplete and inputmode values
Replace every invented autocomplete value with the matching standard token, or with off
where autofill really must be discouraged. Add tokens to fields that collect the visitor's own details,
such as name, email, telephone and address, and use shipping and billing prefixes
on checkout forms with two addresses. Check any inputmode value against the eight allowed
keywords and correct misspellings such as number to numeric.
Step 6
Fix boolean attributes and select placeholders
Search templates for ="false" and ="true" on boolean attributes
and change the template logic so the attribute is printed only when it applies. Give every required select
an empty-value first option with prompt text such as "Choose a size", unless it allows multiple selections
or has a visible size above one. Remove action="" from forms, or set it to the real destination
URL.
Step 7
Submit the form and test autofill
Validate the pages again, then use each form for real. Submit it empty to confirm required fields are caught, submit it filled in and check on the receiving end that every field arrived with the name you expect. Use your browser's saved address and password data to confirm autofill completes the right boxes. Finally, press each non-submit button and make sure none of them submits the form by accident.
Where these errors come from on common platforms
- Form plugins and builders. WordPress form plugins, Shopify apps and hosted form embeds generate the markup for you. Most errors here come from settings rather than code: a field type chosen as "number" for a phone number, or a custom autocomplete value typed into an advanced field. Check the field settings before touching templates.
- Theme templates. Search, newsletter, login and product forms usually live in theme files. Nested forms often appear when a theme's product form and an app's widget are both inserted into the same section.
- Component libraries. A shared input component that accepts every attribute and passes
it straight through will happily print
maxlengthon a number input. Filter props by type inside the component. - Checkout pages on hosted platforms. Checkout markup on hosted e-commerce platforms is often not editable. If errors appear there, record them and move on to the forms you control.
The platform guides for WordPress and Shopify explain how to find and override the template that prints a form on each platform.
1. Does fixing form markup errors improve SEO?
Not directly. Search engines do not rank pages on whether their forms validate. The value is in the form working properly: autofill completing addresses correctly, required fields being caught before submission, and every field arriving at your inbox or system. Those affect enquiries and sales far more than rankings. Correct autocomplete values and working labels also help meet WCAG 2.2, which matters if you have accessibility obligations. Treat form errors as a conversion and accessibility fix rather than a search one.
2. Why does my required field let people submit it empty?
Three common causes. The field may be a select whose first option has a real value, so it is never empty; add an empty-value placeholder option first. The form may have the novalidate attribute, or the submit button formnovalidate, which switches off the browser's own checks. Or a script may submit the form directly, bypassing built-in checking. Invalid markup, such as a nested form moving the field into a different form, can also mean the required field is not the one being checked.
3. How do I stop the browser autofilling a field?
Use autocomplete set to off for fields that should not be filled from saved data, such as a gift message or a one-off reference number. Browsers may still offer suggestions in some cases, particularly for login fields, because they prioritise helping people use password managers. For a new password on a sign-up or change-password form, use new-password instead, which tells password managers to suggest a fresh password. Avoid invented values such as nope, which are invalid and behave unpredictably across browsers.
4. Should phone numbers use type number or type tel?
Use type tel. Phone numbers are not quantities: they can start with zero, contain spaces, brackets or a plus sign, and should never show increment arrows. A number input may reject or mangle those characters, and the validator will flag attributes such as maxlength on it. Type tel shows a phone keypad on mobile devices without imposing number rules, and pairing it with autocomplete set to tel lets browsers fill the visitor's saved number. Validate the format on the server as well.
5. Is it valid to put a form inside another form if I need two submit actions?
No, forms cannot be nested, and the browser ignores the inner form's opening tag, so its fields join the outer form. If you need two different submit actions from one set of fields, give each submit button a formaction attribute pointing at a different URL. If you need a separate small form, such as a voucher code box inside a checkout, place that form elsewhere in the markup and connect its input and button to it using the form attribute with the form's id.
6. Do I need both an id and a name on every input?
They do different jobs. The name is what the server receives, so any field whose value you want submitted must have one; a field without a name is simply left out of the submission. The id is what a label, ARIA reference or script uses to find the field, so it is needed whenever an external label points at the field. Wrapping the input inside its label removes the need for an id, but most teams include both for clarity.
7. The validator error comes from a form plugin. Can I fix it?
Often, yes, through the plugin's settings rather than code. Check each field's type, the autocomplete or HTML attributes option, and whether the same form has been placed twice on the page, which causes duplicate ids. Update the plugin, since many validation issues are fixed in newer versions. If the error is in markup the plugin generates and you cannot change it, report it to the developer, and consider a different plugin if it breaks labels or submissions.
The HTML Checker validates your page against W3C standards and reports errors and warnings covering form semantics, as well as the doctype, encoding, nesting, deprecated elements, duplicate IDs, ARIA and the language attribute.
Open HTML Checker Fixes β