HTML Guides for form
Learn how to identify and fix common HTML validation errors flagged by the W3C Validator — so your pages are standards-compliant and render correctly across every browser. Also check our Accessibility Guides.
HTML forms have built-in constraint validation that is enabled by default. When a user submits a form, the browser automatically checks inputs against their constraints (such as required, type="email", pattern, min, max, etc.) and prevents submission if any validation fails. There is no need to add a validate attribute to opt into this behavior because it is the default.
The HTML specification defines novalidate as a valid boolean attribute on the <form> element to disable this default validation, but it does not define a corresponding validate attribute. Using validate will trigger a W3C validation error because the browser doesn't recognize it and will simply ignore it.
This matters for several reasons:
- Standards compliance: Invalid attributes make your HTML non-conforming, which can cause issues with automated testing and quality tools.
- Developer confusion: A
validateattribute suggests it's doing something functional, but it has no effect. Future maintainers may incorrectly believe it's enabling validation and be reluctant to remove it. - Accessibility and tooling: Screen readers and other assistive technologies rely on well-formed HTML. Unrecognized attributes can lead to unpredictable behavior in some user agents.
To fix this issue, determine your intent:
- If you want form validation enabled — simply remove the
validateattribute. Validation is on by default. - If you want form validation disabled — replace
validatewithnovalidate.
Examples
Incorrect: using the invalid validate attribute
<form validate action="/submit">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<button type="submit">Submit</button>
</form>
This triggers the error: Attribute "validate" not allowed on element "form" at this point.
Correct: relying on default validation (attribute removed)
Since constraint validation is enabled by default, simply remove the invalid attribute:
<form action="/submit">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<button type="submit">Submit</button>
</form>
The browser will automatically validate the email input — checking both the required constraint and that the value matches a valid email format — before allowing submission.
Correct: using novalidate to disable validation
If your intention is to disable built-in validation (for example, because you handle validation with JavaScript), use the novalidate attribute instead:
<form novalidate action="/submit">
<label for="city">City:</label>
<input type="text" id="city" name="city" required>
<button type="submit">Submit</button>
</form>
In this example, even though the input has the required attribute, the browser will not prevent form submission when the field is empty, because novalidate tells the browser to skip constraint validation on submission.
Using formnovalidate on a specific button
If you want validation enabled for normal submission but want to bypass it for a specific button (such as a "Save Draft" button), use the formnovalidate attribute on that button instead of disabling validation for the entire form:
<form action="/submit">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<button type="submit">Submit</button>
<button type="submit" formnovalidate formaction="/save-draft">Save Draft</button>
</form>
The "Submit" button will enforce validation, while the "Save Draft" button will skip it. This approach gives you fine-grained control without needing an invalid attribute.
The autocomplete attribute on a <form> element only accepts "on" or "off" as valid values — "false" is not a recognized keyword.
The autocomplete attribute controls whether the browser can automatically fill in form fields based on previously entered values. When set on a <form> element, it applies as the default behavior for all fields within that form. The valid values are "on" (the browser may auto-complete entries) and "off" (the browser should not auto-complete entries).
It's a common mistake to use "true" or "false" since many other attributes and programming languages use boolean-style values. However, the HTML specification explicitly defines only "on" and "off" for the <form> element's autocomplete attribute.
Note that individual <input> elements support a much wider range of autocomplete values (like "name", "email", "street-address", etc.), but these extended values do not apply to the <form> element itself.
Invalid Example
<form autocomplete="false" action="/submit" method="post">
<label for="email">Email</label>
<input type="email" id="email" name="email">
<button type="submit">Submit</button>
</form>
Valid Example
<form autocomplete="off" action="/submit" method="post">
<label for="email">Email</label>
<input type="email" id="email" name="email">
<button type="submit">Submit</button>
</form>
The action attribute tells the browser where to send form data when the form is submitted. According to the WHATWG HTML living standard, if the action attribute is specified, its value must be a valid non-empty URL potentially surrounded by spaces. An empty string ("") does not satisfy this requirement, which is why the W3C validator flags it.
While some developers use action="" intending the form to submit to the current page's URL, this approach is non-conforming HTML. Browsers do typically interpret an empty action as "submit to the current URL," but relying on this behavior is unnecessary since simply omitting the action attribute achieves the same result in a standards-compliant way. When no action attribute is present, the form submits to the URL of the page containing the form — this is the defined default behavior per the HTML specification.
This issue matters for several reasons:
- Standards compliance: Non-conforming HTML can lead to unexpected behavior across different browsers or future browser versions.
- Maintainability: Using the correct approach makes your intent clearer to other developers. Omitting
actionexplicitly signals "submit to the current page," whileaction=""looks like a mistake or a placeholder that was never filled in. - Tooling: Build tools, linters, and automated testing pipelines that rely on valid HTML may flag or break on this error.
How to Fix
You have two options:
- Remove the
actionattribute if you want the form to submit to the current page URL. - Provide a valid URL if the form should submit to a specific endpoint.
Examples
❌ Invalid: Empty action attribute
<form action="" method="post">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
This triggers the error: Bad value "" for attribute "action" on element "form": Must be non-empty.
✅ Fixed: Remove the action attribute
If you want the form to submit to the current page, simply omit action:
<form method="post">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
✅ Fixed: Provide a valid URL
If the form should submit to a specific endpoint, supply the URL:
<form action="/subscribe" method="post">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
✅ Fixed: Use a hash as a placeholder for JavaScript-handled forms
If your form is entirely handled by JavaScript and should not navigate anywhere, you can use a # as the action or, preferably, omit action and prevent submission in your script:
<form method="post" id="js-form">
<label for="query">Search:</label>
<input type="text" id="query" name="query">
<button type="submit">Search</button>
</form>
document.getElementById('js-form').addEventListener('submit', function(e) {
e.preventDefault();
// Handle form data with JavaScript
});
In this case, omitting action is the cleanest solution. The JavaScript preventDefault() call stops the browser from actually submitting the form, so the action value is never used.
In URLs, certain characters must be percent-encoded — a process where a character is replaced by % followed by exactly two hexadecimal digits representing its byte value. For example, a space becomes %20, a hash becomes %23, and an ampersand becomes %26. The % character itself is the escape prefix, so when a URL parser encounters %, it expects the next two characters to be valid hexadecimal digits (0–9, A–F, a–f). If they aren't, the URL is malformed.
This validation error typically arises in a few scenarios:
- A literal
%is used in the URL without encoding. For instance, a filename or path segment contains a%sign that wasn't converted to%25. - An incomplete or corrupted percent-encoding sequence. Someone may have partially encoded a URL, leaving behind sequences like
%G5or%2that don't form valid two-digit hex codes. - Copy-paste errors. A URL was pasted from a source that stripped characters or introduced stray
%symbols.
This matters because browsers may interpret malformed URLs inconsistently. One browser might try to "fix" the URL by encoding the stray %, while another might pass it through as-is, leading to broken form submissions or unexpected server-side behavior. Ensuring valid URLs in the action attribute guarantees predictable behavior across all browsers and complies with both the WHATWG URL Standard and the HTML specification.
How to Fix
- Locate every
%in the URL. Check whether each%is followed by exactly two hexadecimal digits (e.g.,%20,%3A,%7E). - Encode literal
%characters. If a%is meant to appear as a literal character in the URL (not as part of a percent-encoding sequence), replace it with%25. - Fix incomplete sequences. If a sequence like
%2or%GZexists, determine the intended character and encode it correctly, or remove the stray%. - Use proper encoding tools. In JavaScript, use
encodeURIComponent()orencodeURI()to safely encode URLs. Most server-side languages have equivalent functions (e.g.,urlencode()in PHP,urllib.parse.quote()in Python).
Examples
Literal % not encoded
This triggers the error because %d is not a valid percent-encoding sequence (d is only one character, and what follows may not be hex):
<!-- ❌ Bad: bare % not followed by two hex digits -->
<form action="/search?discount=20%off">
<button type="submit">Search</button>
</form>
Encode the % as %25:
<!-- ✅ Good: literal % encoded as %25 -->
<form action="/search?discount=20%25off">
<button type="submit">Search</button>
</form>
Incomplete percent-encoding sequence
Here, %2 is missing its second hex digit:
<!-- ❌ Bad: %2 is an incomplete sequence -->
<form action="/path/to%2file.html">
<button type="submit">Submit</button>
</form>
If the intent was to encode a / character (%2F), complete the sequence:
<!-- ✅ Good: complete percent-encoding for "/" -->
<form action="/path/to%2Ffile.html">
<button type="submit">Submit</button>
</form>
Invalid hex characters after %
The characters G and Z are not hexadecimal digits:
<!-- ❌ Bad: %GZ is not valid hex -->
<form action="/data%GZprocess">
<button type="submit">Go</button>
</form>
If %GZ was never intended as encoding, escape the % itself:
<!-- ✅ Good: literal % properly encoded -->
<form action="/data%25GZprocess">
<button type="submit">Go</button>
</form>
Using JavaScript to encode safely
When building URLs dynamically, use built-in encoding functions to avoid this issue entirely:
const query = "20% off";
const safeURL = "/search?q=" + encodeURIComponent(query);
// Result: "/search?q=20%25%20off"
This ensures every special character — including % — is properly percent-encoded before it's placed in the action attribute.
The HTML specification requires that if the name attribute is used on a <form> element, its value must be a non-empty string. An empty name="" attribute serves no practical purpose — it doesn't register the form in the document.forms named collection, and it can't be used as a valid reference in scripts. The W3C validator flags this as an error because it violates the content model defined in the WHATWG HTML Living Standard.
The name attribute on a form is primarily used to access the form programmatically through document.forms["formName"]. When the value is empty, this lookup mechanism doesn't work, so the attribute becomes meaningless. This is different from the id attribute, which also identifies elements but participates in fragment navigation and CSS targeting. The name attribute on <form> is specifically for the legacy document.forms named getter interface.
It's worth noting that the name attribute value on a form must not equal an empty string, and it should be unique among the form elements in the forms collection. While duplicate names won't cause a validation error, they can lead to unexpected behavior when accessing forms by name in JavaScript.
How to fix
You have two options:
- Remove the attribute entirely. If you're not referencing the form by name in JavaScript, the
nameattribute is unnecessary. You can useidinstead for CSS or JavaScript targeting. - Provide a meaningful, non-empty value. If you need to reference the form through
document.forms, give it a descriptive name that reflects its purpose.
Examples
❌ Invalid: empty name attribute
<form name="">
<label for="email">Email</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
This triggers the validator error because the name attribute is present but has an empty string value.
✅ Fixed: attribute removed
<form>
<label for="email">Email</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
If you don't need to reference the form by name, simply remove the attribute.
✅ Fixed: meaningful name provided
<form name="subscriptionForm">
<label for="email">Email</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
With a non-empty name, you can now access the form in JavaScript using document.forms["subscriptionForm"] or document.forms.subscriptionForm.
✅ Alternative: using id instead
<form id="subscription-form">
<label for="email">Email</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
In modern development, using id with document.getElementById() or document.querySelector() is often preferred over the name attribute for form identification. The name attribute on <form> is a legacy feature that remains valid but isn't required for most use cases.
The path in a <form> element's action can't contain a literal space, so write each space as %20 or rename the target so its URL has none.
The action attribute holds the URL that receives the submitted form data, and the URL Standard doesn't allow spaces in a path. The validator reports it for values like /contact us/send.php or ./privacy policy.aspx. Accented letters such as í or ñ are allowed in a path, so only the space needs fixing.
Some server frameworks write the current page's file name into action when a form posts back to the same page. If that file name has a space in it, every page built from it reports this error, and renaming the file or route fixes all of them at once.
Browsers percent-encode the space before they submit, so the form usually still works. A crawler or a script that reads the raw value may not do the same, and the markup is invalid either way. Spaces at the very start or end of the value are allowed, because the URL parser strips them. A space after the ? gives a different message, Illegal character in query, and %20 fixes that one too.
Invalid example
<form method="post" action="/contact us/send.php">
<label>
<input type="email" name="email">
</label>
<button type="submit">Send</button>
</form>
Valid example: space encoded as %20
<form method="post" action="/contact%20us/send.php">
<label>
<input type="email" name="email">
</label>
<button type="submit">Send</button>
</form>
Valid example: hyphen instead of a space
<form method="post" action="/contact-us/send.php">
<label>
<input type="email" name="email">
</label>
<button type="submit">Send</button>
</form>
The method attribute on <form> accepts only get, post or dialog, so an empty value or an HTTP verb like PUT or DELETE is invalid.
The keywords are case-insensitive, so POST and post both work, but the value must match one of the three exactly. Extra whitespace, as in method=" post", also triggers this error. dialog only makes sense for a form inside a <dialog> element, where submitting closes the dialog instead of sending a request.
Browsers fall back to GET when the value is invalid. An empty method="" often comes from a template that prints an unset variable. The form still submits as GET, so remove the attribute or write method="get". An unsupported verb causes a real bug: a form with method="PUT" or method="DELETE" sends a GET request with the field values in the URL, so the server's PUT or DELETE handler never runs.
HTML forms can't send PUT, PATCH or DELETE requests. Use method="post" and tell the server which verb you meant. Frameworks such as Rails, Laravel and Phoenix read a hidden _method field for this, or you can send the request with fetch() from JavaScript.
Empty value
Invalid example
<form action="/search" method="">
<label for="q">Search</label>
<input id="q" name="q" type="search">
<button>Search</button>
</form>
Valid example
<form action="/search" method="get">
<label for="q">Search</label>
<input id="q" name="q" type="search">
<button>Search</button>
</form>
Unsupported HTTP verb
Invalid example
<form action="/profile" method="PUT">
<label for="name">Name</label>
<input id="name" name="name">
<button>Save</button>
</form>
Valid example
<form action="/profile" method="post">
<input type="hidden" name="_method" value="put">
<label for="name">Name</label>
<input id="name" name="name">
<button>Save</button>
</form>
Understanding Boolean Attributes in HTML
In HTML, boolean attributes work differently from regular attributes. A boolean attribute's presence on an element represents true, and its absence represents false. According to the HTML specification, a boolean attribute may only have three valid forms:
- The attribute name alone:
novalidate - The attribute with an empty value:
novalidate="" - The attribute with a value matching its own name (case-insensitive):
novalidate="novalidate"
Any other value — such as "true", "false", "1", "0", or "yes" — is invalid and triggers this validation error. This is a common source of confusion, especially for developers coming from frameworks like React (which uses noValidate={true}) or from languages where boolean attributes accept explicit true/false strings.
Why This Matters
- Standards compliance: Using invalid values violates the HTML specification and will cause W3C validation failures.
- Unexpected behavior: While most browsers are lenient and treat any value of
novalidateas "present" (meaning evennovalidate="false"would disable validation, not enable it), relying on this behavior is unreliable and misleading to other developers reading your code. - Maintainability: Writing
novalidate="false"suggests the form should be validated, but the opposite is true — the attribute is present, so validation is skipped. This creates confusing, error-prone code.
About the novalidate Attribute
The novalidate attribute tells the browser to skip its built-in constraint validation when the form is submitted. Without it, the browser checks required fields, input patterns, email formats, and other constraints before allowing submission.
If novalidate is not set on the form, individual submit buttons can still bypass validation on a per-button basis using the formnovalidate attribute on a <button>, <input type="submit">, or <input type="image"> element.
Examples
❌ Invalid: Arbitrary value on novalidate
<form method="post" novalidate="true">
<label>Email:
<input type="email" name="email" required>
</label>
<button>Submit</button>
</form>
This triggers the error because "true" is not a valid value for a boolean attribute. Other invalid variations include:
<form method="post" novalidate="1">
<form method="post" novalidate="yes">
<form method="post" novalidate="false">
Note that novalidate="false" is especially dangerous — it does not enable validation. Because the attribute is present, the browser disables validation regardless of the value.
✅ Valid: Attribute name alone (recommended)
<form method="post" novalidate>
<label>Email:
<input type="email" name="email" required>
</label>
<button>Submit</button>
</form>
✅ Valid: Empty string value
<form method="post" novalidate="">
<label>Email:
<input type="email" name="email" required>
</label>
<button>Submit</button>
</form>
✅ Valid: Value matching the attribute name
<form method="post" novalidate="novalidate">
<label>Email:
<input type="email" name="email" required>
</label>
<button>Submit</button>
</form>
✅ Valid: Removing the attribute to enable validation
If you want the form to be validated, simply remove the novalidate attribute entirely:
<form method="post">
<label>Email:
<input type="email" name="email" required>
</label>
<button>Submit</button>
</form>
✅ Using formnovalidate on a specific button
If you want validation on the primary submit but want to skip it for a "Save Draft" button, use formnovalidate on the button instead:
<form method="post">
<label>Email:
<input type="email" name="email" required>
</label>
<button>Submit</button>
<button formnovalidate>Save Draft</button>
</form>
This pattern keeps validation active for the main submission while allowing drafts to bypass it — without needing novalidate on the form at all.
The type attribute on <input> only accepts the keywords defined in the HTML standard, so any other value, such as phone, social or the name of a website, is reported as an error.
type is an enumerated attribute. The valid keywords are text, search, email, url, tel, password, number, range, date, month, week, time, datetime-local, color, checkbox, radio, file, hidden, submit, reset, button and image. A browser that meets an unknown value treats the field as type="text", so the form still seems to work, but you lose the mobile keyboard, the built-in validation and the autofill hints that the right type gives you.
Pick the keyword that describes the data the field collects, not the topic of the field. A link to a profile page is a url, a phone number is tel, and an address you can send mail to is email. When no type fits, such as a username or a postal code, use type="text" and describe the value with autocomplete, inputmode or pattern. If you need a hook for scripts or styles, put it in name, id, class or a data-* attribute instead of type.
Invalid example
<form>
<label for="profile">Social profile</label>
<input type="social" id="profile" name="profile">
<label for="phone">Phone</label>
<input type="phone" id="phone" name="phone">
</form>
Valid example
<form>
<label for="profile">Social profile</label>
<input type="url" id="profile" name="profile" placeholder="https://example.com/yourname">
<label for="phone">Phone</label>
<input type="tel" id="phone" name="phone" autocomplete="tel">
</form>
A <form> can't contain another <form>, so when the parser finds a second <form> start tag while one is still open, it drops that tag along with its action, method and id.
The fields inside the ignored form then belong to the outer one. In the invalid example below, the "Delete account" button submits the account form to /account, and nothing is ever sent to /account/delete. The inner </form> also closes the outer form early, so any fields after it end up outside any form, and the validator reports the last </form> as Stray end tag “form”.
A common cause is a page template that wraps the whole page in one form, with a search box or newsletter signup inside it that adds a form of its own. A missing </form> causes it too. If the parent element of the first form closes before the form does, the parser still treats that form as active, and it ignores the next <form> on the page.
Move the inner form out so the two forms are siblings. When a button or field has to stay inside one form's layout but submit a different form, give that form an id and point the control at it with the form attribute: <button type="submit" form="delete-account">. The <button>, <input>, <select> and <textarea> elements all accept form. The form it points to can sit anywhere in the document, and it can be empty, as in the valid example below.
Invalid example
<form action="/account" method="post">
<label for="email">Email</label>
<input type="email" id="email" name="email">
<button type="submit">Save</button>
<form action="/account/delete" method="post">
<button type="submit">Delete account</button>
</form>
</form>
Valid example
<form action="/account" method="post">
<label for="email">Email</label>
<input type="email" id="email" name="email">
<button type="submit">Save</button>
<button type="submit" form="delete-account">Delete account</button>
</form>
<form id="delete-account" action="/account/delete" method="post"></form>
A <form> can't sit directly inside a <table>, <thead>, <tbody>, <tfoot> or <tr>: wrap the whole table in the form, or put the form inside a single <td> or <th> cell.
Table elements only accept specific children. A <table> holds <caption>, <colgroup>, <thead>, <tbody>, <tfoot> and <tr>, and a <tr> holds <td> and <th>. A <form> wrapped around one or more rows breaks that structure.
The browser's parser handles this case in a special way. It creates the form as an empty element at the spot where the tag appears, closes it right away, and drops the matching </form> end tag, which the validator reports as a separate Stray end tag “form” error. Browsers still link the fields in the following rows to that empty form, so submitting often appears to work. But the fields are not inside the form in the DOM: a selector like form input won't find them, and if a script moves or re-renders those rows, the fields lose their form.
When each row needs its own form, such as an "Update" button per cart item, keep the <form> elements outside the table and link each field and button to its form with the form attribute, which takes the form's id.
Invalid example
<table>
<form action="/cart" method="post">
<tr>
<td><label for="qty">Quantity</label></td>
<td><input type="number" id="qty" name="qty"></td>
<td><button type="submit">Update</button></td>
</tr>
</form>
</table>
Valid example
<form action="/cart" method="post">
<table>
<tr>
<td><label for="qty">Quantity</label></td>
<td><input type="number" id="qty" name="qty"></td>
<td><button type="submit">Update</button></td>
</tr>
</table>
</form>
Valid example with one form per row
<form id="item-1" action="/cart/1" method="post"></form>
<form id="item-2" action="/cart/2" method="post"></form>
<table>
<tr>
<td><input type="number" name="qty" form="item-1" aria-label="Quantity for item 1"></td>
<td><button type="submit" form="item-1">Update</button></td>
</tr>
<tr>
<td><input type="number" name="qty" form="item-2" aria-label="Quantity for item 2"></td>
<td><button type="submit" form="item-2">Update</button></td>
</tr>
</table>
An <input> can't sit directly inside a <table>, <thead>, <tbody>, <tfoot> or <tr>: put it inside a <td> or <th> cell, or move it outside the table.
Table elements only accept specific children. A <table> holds <caption>, <colgroup>, <thead>, <tbody>, <tfoot> and <tr>, and a <tr> holds <td> and <th>.
When the browser's parser finds a misplaced <input>, it moves the element out and inserts it just before the table. The HTML specification calls this foster parenting. The field then shows up above the table instead of in the row where you wrote it. Hidden inputs (type="hidden") are the exception: the parser leaves them in place, but the validator flags them too.
This usually comes from forms laid out with tables, where hidden fields such as CSRF tokens or page numbers end up between <tr> elements. Move hidden inputs before or after the <table>, keeping them inside the <form>, and put visible inputs in a cell. If the <form> tag itself sits inside the table, move it out so it wraps the whole <table>. A <form> between rows triggers its own validation error.
Invalid example
<form action="/search">
<table>
<tr>
<td><label for="q">Search</label></td>
<td><input type="text" id="q" name="q"></td>
</tr>
<input type="hidden" name="page" value="1">
</table>
</form>
Valid example
<form action="/search">
<input type="hidden" name="page" value="1">
<table>
<tr>
<td><label for="q">Search</label></td>
<td><input type="text" id="q" name="q"></td>
</tr>
</table>
</form>
Many HTML elements come with built-in (implicit) ARIA roles that browsers and assistive technologies already recognize. The <form> element natively maps to the form ARIA role, meaning screen readers and other tools already understand it as a form landmark without any extra attributes. When you explicitly add role="form" to a <form> element, you're telling the browser something it already knows.
This redundancy is problematic for several reasons:
- Code clarity: Unnecessary attributes make your HTML harder to read and maintain. Other developers may wonder if the explicit role is there to override something or if it serves a special purpose.
- Misleading intent: Explicit ARIA roles are typically reserved for cases where you need to override or supplement the default semantics of an element. Using them unnecessarily can signal to future maintainers that something unusual is happening when it isn't.
- ARIA best practices: The first rule of ARIA is "do not use ARIA if you can use a native HTML element or attribute with the semantics and behavior you require." Adding redundant ARIA roles goes against this principle.
It's worth noting that the <form> element's implicit form role only exposes it as a landmark when the form has an accessible name (e.g., via aria-label or aria-labelledby). If you need your form to appear as a landmark region, provide an accessible name rather than adding a redundant role.
To fix this issue, simply remove role="form" from any <form> element. If you want the form to function as a named landmark for assistive technology users, add an accessible name instead.
Examples
❌ Incorrect: redundant role="form"
<form role="form" action="/subscribe" method="post">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
This triggers the validator warning because role="form" duplicates the element's implicit role.
✅ Correct: no explicit role
<form action="/subscribe" method="post">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
The <form> element already communicates its role natively. No ARIA attribute is needed.
✅ Correct: form with an accessible name for landmark navigation
<form action="/subscribe" method="post" aria-label="Newsletter subscription">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
If you want the form to be discoverable as a named landmark by screen reader users, provide an aria-label or aria-labelledby attribute — not a redundant role.
Other elements with implicit roles
The same principle applies to many other HTML elements. Avoid adding redundant roles like these:
<!-- ❌ Redundant roles -->
<nav role="navigation">...</nav>
<main role="main">...</main>
<header role="banner">...</header>
<footer role="contentinfo">...</footer>
<button role="button">Click me</button>
<!-- ✅ Let native semantics do the work -->
<nav>...</nav>
<main>...</main>
<header>...</header>
<footer>...</footer>
<button>Click me</button>
Trust the native semantics of HTML elements. Only use explicit ARIA roles when you genuinely need to change or supplement an element's default behavior.
The accept-charset attribute on a <form> element only accepts "UTF-8" as its value. Any other character encoding will trigger a validation error.
Older HTML specifications allowed values like "ISO-8859-1", "Windows-1252", or space-separated lists of encodings. The HTML living standard changed this. Now, "UTF-8" is the sole permitted value. Browsers already default to submitting form data in UTF-8 when no accept-charset is specified, so the attribute is rarely needed at all.
If a form currently specifies a non-UTF-8 encoding, the fix is either to switch the value to "UTF-8" or to remove the attribute entirely. Removing it is usually the better choice, since the browser will use UTF-8 by default when the page itself is served as UTF-8 (which it should be).
HTML examples
Invalid usage
<form action="/search" accept-charset="ISO-8859-1">
<input type="text" name="q">
<button type="submit">Search</button>
</form>
Valid usage
Remove the attribute and let the browser default to UTF-8:
<form action="/search">
<input type="text" name="q">
<button type="submit">Search</button>
</form>
Or explicitly set it to "UTF-8":
<form action="/search" accept-charset="UTF-8">
<input type="text" name="q">
<button type="submit">Search</button>
</form>
Validate at scale.
Ship accessible websites, faster.
Automated HTML & accessibility validation for large sites. Check thousands of pages against WCAG guidelines and W3C standards in minutes, not days.
1K Weekly
1,000 monthly checks. Cancel anytime.
Get 1K Weekly →Join teams across 40+ countries