HTML Guides
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.
The autocomplete attribute helps browsers autofill form fields with previously saved user data. The HTML specification defines a strict set of valid values, and each one maps to a specific type of information (like a name, email address, phone number, or street address). The string "contact" by itself is not a valid autofill field name — it's a contact type token, which is a modifier meant to be combined with a field name to distinguish between different types of contact information.
The HTML spec defines two contact type tokens: "home", "work", "mobile", "fax", and "pager" (for phone-related fields), as well as the broader "shipping" and "billing" scoping tokens. The token "contact" doesn't exist as a standalone value at all. You may have confused it with a contact type prefix pattern like "home email" or "work tel", or you may have intended to use a specific field name entirely.
Getting the autocomplete value right matters for several reasons. Browsers rely on these exact tokens to offer relevant autofill suggestions. Screen readers and assistive technologies may also use this information to help users understand what data a field expects. An invalid value means the browser will likely ignore the attribute entirely, degrading the user experience — especially on mobile devices where autofill is heavily used.
To fix the issue, determine what kind of information the input field is collecting and use the appropriate autofill field name. Common valid values include "name", "email", "tel", "street-address", "postal-code", "organization", and "username". If you want to indicate that this is specifically a contact email or phone (as opposed to, say, a billing one), you don't use "contact" — instead, you can omit the modifier entirely or use a section-scoping approach.
Examples
❌ Invalid: Using "contact" as the autocomplete value
<labelfor="email">Contact Email</label>
<inputtype="email"id="email"name="email"autocomplete="contact">
The value "contact" is not a recognized autofill field name, so the browser cannot determine what to autofill.
✅ Fixed: Using a valid autofill field name
<labelfor="email">Contact Email</label>
<inputtype="email"id="email"name="email"autocomplete="email">
The value "email" is a valid autofill field name that tells the browser to suggest saved email addresses.
✅ Fixed: Using a valid combination with a section or contact type token
If you need to differentiate between types of phone numbers, you can use tokens like "home", "work", or "mobile" as prefixes:
<labelfor="work-tel">Work Phone</label>
<inputtype="tel"id="work-tel"name="work-tel"autocomplete="work tel">
<labelfor="home-email">Personal Email</label>
<inputtype="email"id="home-email"name="home-email"autocomplete="home email">
Common valid autocomplete values
Here are some frequently used valid autofill field names:
| Value | Purpose |
|---|---|
"name" | Full name |
"email" | Email address |
"tel" | Phone number |
"username" | Username |
"new-password" | New password (for registration) |
"current-password" | Existing password (for login) |
"street-address" | Street address |
"postal-code" | ZIP or postal code |
"country-name" | Country name |
"organization" | Company or organization |
"off" | Disable autofill |
For the complete list of valid values and their permitted combinations, refer to the WHATWG autofill specification.
The http-equiv attribute on the <meta> element simulates HTTP response headers. However, the HTML living standard only allows a specific set of values for http-equiv, and Content-Script-Type is not among them. The allowed values include content-type, default-style, refresh, x-ua-compatible, and content-security-policy.
In HTML 4.01, <meta http-equiv="Content-Script-Type" content="text/javascript"> was used to tell the browser which scripting language to assume for inline event handlers (like onclick). Since JavaScript is now the only scripting language supported by browsers, this declaration serves no purpose. Every modern browser already assumes JavaScript by default, making this meta tag completely redundant.
Removing this tag has no effect on your page's behavior. Your scripts will continue to work exactly as before. If you're maintaining a legacy codebase, you can safely delete this line during any cleanup or modernization effort.
Examples
❌ Invalid: using Content-Script-Type
<head>
<metacharset="utf-8">
<metahttp-equiv="Content-Script-Type"content="text/javascript">
<title>My Page</title>
</head>
This triggers the validation error because Content-Script-Type is not a valid http-equiv value in modern HTML.
✅ Fixed: remove the obsolete meta tag
<head>
<metacharset="utf-8">
<title>My Page</title>
</head>
Simply remove the <meta http-equiv="Content-Script-Type"> line. No replacement is needed — browsers already default to JavaScript for all script handling.
✅ Valid http-equiv values for reference
Here are some examples of http-equiv values that are valid in modern HTML:
<head>
<metacharset="utf-8">
<metahttp-equiv="x-ua-compatible"content="IE=edge">
<metahttp-equiv="refresh"content="30">
<metahttp-equiv="content-security-policy"content="default-src 'self'">
<title>My Page</title>
</head>
The http-equiv attribute accepts a specific set of predefined values, and the validator checks both the value itself and its formatting. When the validator reports a bad value of ""Content-Security-Policy"" (note the doubled quotes), it means the actual attribute value being parsed includes literal quotation mark characters as part of the string. The browser sees the first " as opening the attribute, then immediately sees the second " as closing it — resulting in a malformed tag that won't work as intended.
This matters for several reasons. Content-Security-Policy delivered via a <meta> tag is a critical security mechanism that restricts which resources your page can load. If the tag is malformed, the browser will silently ignore the policy, leaving your site without the CSP protections you intended. There's no visual indication that the policy failed to apply, making this a particularly dangerous bug.
Common causes of this issue include:
- Copying code from a word processor or CMS that converts straight quotes (
") into curly/smart quotes ("and"). - Double-escaping in templates where a templating engine adds quotes around a value that already has quotes in the markup.
- Manual typos where quotes are accidentally duplicated.
To fix this, open your HTML source in a plain-text editor (not a word processor) and ensure the http-equiv value is wrapped in exactly one pair of standard straight double quotes with no extra quote characters inside.
Examples
Incorrect — doubled quotes around the value
<metahttp-equiv=""Content-Security-Policy"" content="default-src 'self';">
The validator interprets this as an http-equiv attribute with an empty value (""), followed by unrecognized content (Content-Security-Policy""), producing the error.
Incorrect — curly/smart quotes
<metahttp-equiv="Content-Security-Policy"content="default-src 'self';">
Smart quotes (" and ") are not valid attribute delimiters in HTML. They become part of the attribute value itself, causing the validator to reject it.
Incorrect — HTML entity quotes inside the attribute
<metahttp-equiv=""Content-Security-Policy""content="default-src 'self';">
Using " inside the attribute value embeds literal quote characters into the value string, which makes it invalid.
Correct — single pair of straight double quotes
<metahttp-equiv="Content-Security-Policy"content="default-src 'self';">
Correct — full document example
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<metahttp-equiv="Content-Security-Policy"content="default-src 'self'; img-src https:; script-src 'self';">
<title>CSP Example</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
The http-equiv value Content-Security-Policy must be spelled exactly as shown — it is case-insensitive per the HTML spec, but using the canonical casing is recommended for clarity. The actual policy directives go in the content attribute, not in http-equiv. If you're using a templating engine or CMS, check the generated HTML source (via "View Page Source" in your browser) to confirm the output contains clean, straight quotes with no doubling.
Why This Is an Issue
In HTML 4 and XHTML, the Content-Style-Type HTTP header (and its <meta http-equiv> equivalent) told the browser which stylesheet language to use when interpreting inline style attributes and <style> elements. This was theoretically necessary because the specification allowed for alternative stylesheet languages beyond CSS.
In practice, CSS became the only stylesheet language browsers support. The HTML living standard (maintained by WHATWG) recognizes this reality and defines a strict list of valid http-equiv values. Content-Style-Type is not among them. The only valid values include content-type, default-style, refresh, x-ua-compatible, content-security-policy, and a few others defined in the spec.
Because every browser defaults to CSS for all styling, this meta tag serves no functional purpose. Keeping it in your markup only produces a validation error and adds unnecessary bytes to your document.
Similarly, the related Content-Script-Type meta tag (which declared the default scripting language) is also obsolete for the same reasons — JavaScript is the universal default.
How to Fix It
The fix is straightforward: remove the <meta http-equiv="Content-Style-Type" ...> tag from your document's <head>. No replacement is needed. Browsers will interpret all stylesheets as CSS and all scripts as JavaScript without any explicit declaration.
If you inherited this tag from a legacy template or an older CMS, you can safely delete it with no impact on your site's appearance or behavior.
Examples
❌ Invalid: Using the obsolete Content-Style-Type pragma
<head>
<metacharset="utf-8">
<metahttp-equiv="Content-Style-Type"content="text/css">
<title>My Page</title>
</head>
This triggers the validator error: Bad value "Content-Style-Type" for attribute "http-equiv" on element "meta".
✅ Valid: Simply remove the obsolete meta tag
<head>
<metacharset="utf-8">
<title>My Page</title>
</head>
No replacement is needed. CSS is already the default stylesheet language in all browsers.
❌ Invalid: Both Content-Style-Type and Content-Script-Type (common in legacy templates)
<head>
<metacharset="utf-8">
<metahttp-equiv="Content-Style-Type"content="text/css">
<metahttp-equiv="Content-Script-Type"content="text/javascript">
<title>Legacy Page</title>
</head>
✅ Valid: Remove both obsolete declarations
<head>
<metacharset="utf-8">
<title>Legacy Page</title>
</head>
Both Content-Style-Type and Content-Script-Type are obsolete. Removing them has zero effect on how browsers render your page, and your HTML will pass validation cleanly.
The lang attribute on any HTML element must be a valid IANA language subtag (like en, fr, or ja), not a programming language name.
The lang attribute specifies the natural (human) language of the element's content, used by browsers, screen readers, and search engines for accessibility and localization purposes. Values like csharp, javascript, or python are not valid because they are not human languages registered in the IANA Language Subtag Registry.
This issue commonly arises when using <code lang="csharp"> to indicate the programming language of a code snippet. While the intention makes sense, lang is not the right attribute for this purpose.
To indicate a programming language on a <code> element, the recommended convention from the HTML specification is to use the class attribute with a language- prefix (e.g., class="language-csharp"). This is also the pattern used by popular syntax highlighting libraries like Prism.js and highlight.js.
If the code content is written in a specific human language (like English), you can still use lang="en" alongside the class.
Bad Example
<pre>
<codelang="csharp">
Console.WriteLine("Hello, World!");
</code>
</pre>
Good Example
<pre>
<codeclass="language-csharp">
Console.WriteLine("Hello, World!");
</code>
</pre>
The datetime input type was originally proposed to allow users to enter a date and time along with a timezone. However, no browser ever fully implemented it with a usable UI, and the WHATWG HTML Living Standard officially dropped it. Because datetime is not a recognized value for the type attribute, browsers treat <input type="datetime"> as <input type="text"> — a plain text field with no built-in date or time validation, no native picker, and no semantic meaning for assistive technologies.
This matters for several reasons. From an accessibility standpoint, screen readers and other assistive technologies rely on the correct type attribute to communicate the purpose of an input to users. A fallback to type="text" provides no such context. From a validation and user experience perspective, native date/time input types offer built-in pickers on mobile and desktop browsers, enforce format constraints, and support attributes like min, max, and step — none of which work correctly when the type falls back to plain text. From a standards compliance perspective, using an obsolete type means your HTML is invalid, which can cause issues with automated testing, SEO audits, and quality assurance processes.
How to Fix It
Choose a replacement based on what data you need to collect:
type="datetime-local"— Collects both a date and a time without timezone information. This is the most direct replacement for the olddatetimetype.type="date"— Collects only a date (year, month, day).type="time"— Collects only a time (hours, minutes, optionally seconds).type="text"with a JavaScript widget — Use this approach if you need timezone-aware datetime input, since no native HTML input type currently handles timezones.
The datetime-local input supports useful attributes like min, max, step, value, and name, giving you control over the range and granularity of selectable values. The step attribute is specified in seconds (e.g., step="60" for one-minute intervals, step="1" to allow seconds).
Examples
❌ Invalid: Using the obsolete datetime type
This triggers the W3C validation error:
<form>
<label>
Meeting:
<inputtype="datetime"name="meeting">
</label>
</form>
✅ Fixed: Using datetime-local
Replace datetime with datetime-local to collect both a date and time:
<form>
<label>
Meeting:
<inputtype="datetime-local"name="meeting"step="60">
</label>
</form>
✅ Fixed: Using separate date and time inputs
If you prefer to collect the date and time independently, split them into two fields:
<form>
<label>
Meeting date:
<inputtype="date"name="meeting-date"min="2025-01-01"max="2025-12-31">
</label>
<label>
Meeting time:
<inputtype="time"name="meeting-time"step="900">
</label>
</form>
In this example, step="900" on the time input sets 15-minute intervals (900 seconds).
✅ Fixed: Using datetime-local with constraints
You can set minimum and maximum allowed values using the standard datetime-local format (YYYY-MM-DDThh:mm):
<form>
<label>
Appointment:
<input
type="datetime-local"
name="appointment"
min="2025-06-01T09:00"
max="2025-06-30T17:00"
step="1800"
required>
</label>
<buttontype="submit">Book</button>
</form>
This restricts selection to June 2025 during business hours, in 30-minute increments, and makes the field required.
The HTML specification defines a set of implicit ARIA roles (also called "native semantics") for many HTML elements. The <dialog> element's implicit role is dialog, which means assistive technologies like screen readers already announce it correctly without any explicit ARIA markup. When you add role="dialog" to a <dialog> element, you're restating what the browser and accessibility tree already know—and the ARIA in HTML specification explicitly restricts this.
The ARIA in HTML spec maintains a list of allowed roles for each HTML element. For <dialog>, the only permitted role override is alertdialog (for dialogs that require an immediate response from the user). Setting role="dialog" is not listed as an allowed value because it duplicates the native semantics, and the spec treats such redundancy as a conformance error. This is why the W3C Validator reports: Bad value "dialog" for attribute "role" on element "dialog".
Why this matters
- Standards compliance: The W3C Validator enforces the ARIA in HTML specification, which prohibits redundant role assignments on elements that already carry that role implicitly. Valid markup ensures your pages conform to web standards.
- Accessibility clarity: While most assistive technologies handle redundant roles gracefully today, unnecessary ARIA attributes add noise to the codebase and can cause confusion about whether the element's native semantics are intentionally being overridden. The first rule of ARIA is: don't use ARIA if a native HTML element already provides the semantics you need.
- Maintainability: Removing redundant attributes keeps your HTML clean and easier to maintain. Future developers won't need to wonder whether the explicit role was added intentionally to work around a bug.
How to fix it
- Locate any
<dialog>element with arole="dialog"attribute. - Remove the
roleattribute entirely. - If you need the dialog to behave as an alert dialog (one that interrupts the user and demands immediate attention), use
role="alertdialog"instead—this is the one permitted role override for<dialog>.
Examples
Incorrect — redundant role causes a validation error
<dialogrole="dialog">
<h2>Confirm action</h2>
<p>Are you sure you want to proceed?</p>
<button>Cancel</button>
<button>Confirm</button>
</dialog>
Correct — relying on the implicit role
<dialog>
<h2>Confirm action</h2>
<p>Are you sure you want to proceed?</p>
<button>Cancel</button>
<button>Confirm</button>
</dialog>
The <dialog> element automatically exposes role="dialog" in the accessibility tree, so no explicit attribute is needed.
Correct — using an allowed role override
If the dialog represents an urgent alert that requires immediate user interaction, you can override the role with alertdialog:
<dialogrole="alertdialog"aria-labelledby="alert-title"aria-describedby="alert-desc">
<h2id="alert-title">Session expiring</h2>
<pid="alert-desc">Your session will expire in 60 seconds. Do you want to continue?</p>
<button>Stay signed in</button>
</dialog>
This is valid because alertdialog is explicitly listed as a permitted role for the <dialog> element in the ARIA in HTML specification. Note that aria-labelledby and aria-describedby are strongly recommended for alert dialogs so assistive technologies can announce the title and description properly.
The HTML <input> element's type attribute only accepts a specific set of predefined values defined in the HTML specification. These include values like text, password, email, number, date, datetime-local, checkbox, radio, and others. The value dob — presumably short for "date of birth" — is not among them.
When a browser encounters an invalid type value, it doesn't throw an error or prevent the page from loading. Instead, it treats the input as type="text". This means the input might appear to work, but you lose important benefits: there's no native date picker UI, no built-in date format validation, and no appropriate mobile keyboard. The W3C validator flags this to help you catch the mistake early.
This matters for several reasons:
- Accessibility: Valid input types provide semantic meaning to assistive technologies. A
type="date"input tells screen readers that a date is expected, enabling better guidance for users. - User experience: Native date inputs offer platform-appropriate date pickers on mobile and desktop, reducing input errors.
- Standards compliance: Using invalid attribute values produces unpredictable behavior across browsers and can break future compatibility.
To fix this issue, replace type="dob" with a recognized type. For a date of birth field, type="date" is the most appropriate choice. If you need more control over formatting, you can use type="text" with a JavaScript date picker library or custom validation.
Examples
❌ Invalid: using type="dob"
<labelfor="dob">Date of Birth:</label>
<inputtype="dob"id="dob"name="dob">
The browser will treat this as a plain text input, and the W3C validator will report: Bad value "dob" for attribute "type" on element "input".
✅ Fixed: using type="date"
<labelfor="dob">Date of Birth:</label>
<inputtype="date"id="dob"name="dob">
This uses the native HTML date input, which provides a built-in date picker in most modern browsers. You can also constrain the date range with min and max attributes:
<labelfor="dob">Date of Birth:</label>
<inputtype="date"id="dob"name="dob"min="1900-01-01"max="2025-12-31">
✅ Fixed: using type="text" with a JavaScript date picker
If you need more control over the date picker's appearance or need to support older browsers that lack native date input support, use type="text" and enhance it with JavaScript:
<labelfor="dob">Date of Birth:</label>
<inputtype="text"id="dob"name="dob"placeholder="YYYY-MM-DD">
You can then attach a JavaScript date picker library (such as Flatpickr, Pikaday, or a framework-specific component) to this input for a custom date selection experience. When using this approach, make sure to add appropriate aria-* attributes and validation to maintain accessibility.
Valid type values for reference
Here are some commonly used valid type values for the <input> element:
text— plain text inputdate— date picker (year, month, day)datetime-local— date and time picker (no timezone)month— month and year pickernumber— numeric inputemail— email address inputtel— telephone number inputpassword— masked text input
Always choose the type that best matches the data you're collecting. For a date of birth, type="date" is the most semantically correct and user-friendly option.
The hreflang attribute on a link element tells browsers and search engines the language and optional region of the linked resource. Its value must be a valid BCP 47 language tag, which follows a specific structure: a primary language subtag (like en, fr, de) optionally followed by a region subtag (like US, GB, FR). The region subtag must be a valid two-letter ISO 3166-1 alpha-2 country code.
The tag en-EN is invalid because EN is not a recognized country code. There is no country with the code "EN" — it's a common mistake where the language code is simply repeated as the region. This pattern shows up frequently with other languages too, such as fr-FR (which happens to be valid because FR is the country code for France) leading people to assume en-EN follows the same logic. However, EN is not assigned to any country in ISO 3166-1, so the validator correctly rejects it.
Why This Matters
- SEO and internationalization: Search engines like Google use
hreflangvalues to serve the correct localized version of a page. An invalid value may cause search engines to ignore the tag entirely, undermining your localization strategy. - Standards compliance: Browsers and tools rely on well-formed language tags to apply correct locale-specific behavior such as font selection, date formatting hints, and spell-checking language.
- Accessibility: Screen readers and assistive technologies may use language information to select the correct speech synthesis voice. Invalid language tags can lead to content being read in the wrong accent or language.
Common Invalid Region Subtags
This mistake isn't limited to English. Here are some commonly seen invalid patterns and their corrections:
| Invalid | Why It's Wrong | Valid Alternatives |
|---|---|---|
en-EN | EN is not a country code | en, en-US, en-GB, en-AU |
de-DE | ✅ Actually valid — DE is Germany | de, de-DE, de-AT, de-CH |
ja-JA | JA is not a country code | ja, ja-JP |
zh-ZH | ZH is not a country code | zh, zh-CN, zh-TW |
ko-KO | KO is not a country code | ko, ko-KR |
The key takeaway: don't assume you can double the language code to make a region subtag. Always verify against the ISO 3166-1 alpha-2 country code list.
How to Fix It
- If you don't need a regional variant, just use the bare language subtag:
en. - If you need a specific regional variant, pair the language subtag with the correct country code:
en-USfor American English,en-GBfor British English,en-AUfor Australian English, etc. - Look up the country code if you're unsure. The region subtag corresponds to a country, not a language.
Examples
Incorrect — Invalid region subtag
<linkrel="alternate"href="https://example.com/en/"hreflang="en-EN">
The region subtag EN does not correspond to any country and will trigger a validation error.
Correct — Language only
If the content is simply in English without a specific regional variant, omit the region:
<linkrel="alternate"href="https://example.com/en/"hreflang="en">
Correct — Language with valid region subtags
When you need to differentiate between regional variants, use proper country codes:
<linkrel="alternate"href="https://example.com/en-us/"hreflang="en-US">
<linkrel="alternate"href="https://example.com/en-gb/"hreflang="en-GB">
<linkrel="alternate"href="https://example.com/en-au/"hreflang="en-AU">
Correct — Full set of hreflang links with x-default
A typical multilingual setup with properly formed language tags:
<linkrel="alternate"href="https://example.com/"hreflang="x-default">
<linkrel="alternate"href="https://example.com/en/"hreflang="en">
<linkrel="alternate"href="https://example.com/en-us/"hreflang="en-US">
<linkrel="alternate"href="https://example.com/fr/"hreflang="fr">
<linkrel="alternate"href="https://example.com/de/"hreflang="de">
<linkrel="alternate"href="https://example.com/ja/"hreflang="ja">
Note the use of x-default to indicate the default or language-selection page — this is a special value recognized by search engines for fallback purposes.
The http-equiv attribute on the <meta> element is designed to simulate certain HTTP response headers directly in HTML. However, the HTML specification only permits a specific set of values: content-type, default-style, refresh, x-ua-compatible, and content-security-policy. Using Expires as a value for http-equiv will trigger a validation error because it falls outside this permitted set.
Historically, some older browsers and HTML versions were more lenient about which values could appear in http-equiv, and developers commonly used <meta http-equiv="Expires" content="0"> or similar patterns to try to prevent page caching. However, this approach was never reliable — browsers and caching proxies handle actual HTTP headers far more consistently than <meta> tag equivalents. Modern HTML formally disallows this value.
Beyond standards compliance, there are practical reasons to avoid this pattern. Many browsers simply ignore unrecognized http-equiv values, meaning the tag does nothing useful. Cache behavior is best controlled at the HTTP level, where servers, CDNs, and proxies all read and respect the headers. Relying on a <meta> tag for caching gives a false sense of control while cluttering your markup with invalid code.
To fix this issue, remove the <meta http-equiv="Expires" ...> tag from your HTML and configure the Expires or Cache-Control HTTP header on your web server.
Examples
Incorrect: Using Expires in http-equiv
This triggers the validation error:
<head>
<metacharset="UTF-8">
<metahttp-equiv="Expires"content="0">
<metahttp-equiv="Expires"content="Tue, 01 Jan 2025 00:00:00 GMT">
<title>My Page</title>
</head>
Correct: Remove the invalid <meta> tag
Simply remove the offending tag. Only use valid http-equiv values:
<head>
<metacharset="UTF-8">
<title>My Page</title>
</head>
Correct: Valid uses of http-equiv
For reference, here are examples of valid http-equiv values:
<head>
<metacharset="UTF-8">
<metahttp-equiv="refresh"content="30">
<metahttp-equiv="content-security-policy"content="default-src 'self'">
<metahttp-equiv="default-style"content="main-stylesheet">
<title>My Page</title>
</head>
Correct: Set cache expiration via server configuration
The proper way to control caching is through HTTP response headers configured on your server.
Apache (.htaccess or server config):
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/html "access plus 1 day"
</IfModule>
Or using Cache-Control with mod_headers:
<IfModule mod_headers.c>
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Expires "0"
</IfModule>
Nginx:
location~*\.html$ {
expires1d;
add_headerCache-Control"public, no-transform";
}
To prevent caching entirely in Nginx:
location~*\.html$ {
expires-1;
add_headerCache-Control"no-store, no-cache, must-revalidate";
}
If you don't have access to server configuration, many server-side languages let you set headers programmatically. For example, in PHP:
<?php
header("Expires: Tue, 01 Jan 2030 00:00:00 GMT");
header("Cache-Control: public, max-age=86400");
?>
By handling cache expiration at the server level, you get reliable behavior across all browsers, proxies, and CDNs — while keeping your HTML clean and standards-compliant.
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
<formautocomplete="false"action="/submit"method="post">
<labelfor="email">Email</label>
<inputtype="email"id="email"name="email">
<buttontype="submit">Submit</button>
</form>
Valid Example
<formautocomplete="off"action="/submit"method="post">
<labelfor="email">Email</label>
<inputtype="email"id="email"name="email">
<buttontype="submit">Submit</button>
</form>
The hidden attribute indicates that an element is not yet, or is no longer, relevant to the current state of the page. Browsers will not render elements that have this attribute. It's available on all HTML elements as a global attribute.
In HTML, boolean attributes like hidden, disabled, readonly, and checked follow special rules. Unlike attributes in programming languages where you might set a value to true or false, boolean attributes in HTML work by presence or absence:
- Present = the feature is on (e.g.,
hidden,hidden="", orhidden="hidden") - Absent = the feature is off (the attribute is simply not in the markup)
This is a common source of confusion. Writing hidden="false" does not make the element visible. Because the attribute is still present in the markup, the browser interprets it as "this element is hidden." The actual string value "false" is ignored for the purpose of determining the boolean state. This can lead to frustrating bugs where elements remain invisible despite what looks like correct code.
According to the HTML specification, the only valid values for a boolean attribute are the empty string ("") or the attribute's own name (e.g., hidden="hidden"). Any other value, including "true" or "false", is invalid and will trigger a W3C validator error.
How the hidden attribute works with newer values
Starting with more recent updates to the HTML specification, the hidden attribute also accepts the value "until-found". When set to hidden="until-found", the element remains hidden but can be revealed by the browser's find-in-page feature or by fragment navigation. This is the only keyword value (besides the empty string and the attribute's canonical name) that changes the attribute's behavior. It does not change the fact that "false" is an invalid value.
How to fix it
- To hide an element, add the
hiddenattribute with no value. - To show an element, remove the
hiddenattribute entirely from the markup. - If you're toggling visibility with JavaScript, use
element.hidden = false(the JavaScript property, not the HTML attribute) orelement.removeAttribute('hidden').
Examples
❌ Invalid: setting hidden to "false"
<!-- The element is STILL hidden and the markup is invalid -->
<divhidden="false">You won't see this text.</div>
❌ Invalid: setting hidden to "true"
<!-- "true" is also not a valid value for a boolean attribute -->
<phidden="true">This paragraph is hidden, but the markup is invalid.</p>
✅ Valid: using hidden without a value
<divhidden>This element is hidden from the page.</div>
✅ Valid: using hidden with an empty string or its own name
<!-- Both of these are valid ways to write boolean attributes -->
<divhidden="">Hidden element</div>
<divhidden="hidden">Also a hidden element</div>
✅ Valid: showing the element by omitting hidden
<div>This element is visible because it has no hidden attribute.</div>
✅ Valid: using hidden="until-found"
<divhidden="until-found">
This content is hidden but can be found via browser search.
</div>
Toggling visibility with JavaScript
When dynamically showing or hiding elements, use the hidden property on the DOM element rather than setting the attribute to "false":
<buttontype="button"id="toggle">Toggle message</button>
<pid="message"hidden>Hello! Now you can see me.</p>
<script>
document.getElementById("toggle").addEventListener("click",function(){
constmsg=document.getElementById("message");
msg.hidden=!msg.hidden;// Correctly toggles the boolean property
});
</script>
Using msg.hidden = false in JavaScript correctly removes the hidden attribute from the element. This is different from writing hidden="false" directly in HTML, which keeps the attribute present and triggers the validation error.
MIME types (also called media types) follow a strict type/subtype structure as defined by IETF standards. For example, image/png has the type image and the subtype png. The value "favicon" doesn't follow this format — it has no slash and no subtype — so the validator reports "Subtype missing." This is a common mistake that happens when developers confuse the purpose of the icon (a favicon) with the format of the file (an image in a specific encoding).
While most browsers are forgiving and will still display the favicon even with an invalid type value, using incorrect MIME types can cause issues. Some browsers or tools may ignore the <link> element entirely if the type doesn't match a recognized format. It also hurts standards compliance and makes your HTML less predictable across different environments.
The correct MIME type depends on the file format of your favicon:
.icofiles:image/x-icon(orimage/vnd.microsoft.icon).pngfiles:image/png.svgfiles:image/svg+xml.giffiles:image/gif
It's also worth noting that the type attribute on <link rel="icon"> is entirely optional. If omitted, the browser will determine the file type from the server's Content-Type header or by inspecting the file itself. Removing it is a perfectly valid fix.
Examples
❌ Invalid: using "favicon" as the type
<linkrel="icon"href="/favicon.png"type="favicon">
The value "favicon" is not a valid MIME type, triggering the validation error.
✅ Fixed: using the correct MIME type for a PNG favicon
<linkrel="icon"href="/favicon.png"type="image/png">
✅ Fixed: using the correct MIME type for an ICO favicon
<linkrel="icon"href="/favicon.ico"type="image/x-icon">
✅ Fixed: using the correct MIME type for an SVG favicon
<linkrel="icon"href="/favicon.svg"type="image/svg+xml">
✅ Fixed: omitting the type attribute entirely
<linkrel="icon"href="/favicon.png">
Since the type attribute is optional for <link rel="icon">, removing it avoids the error and lets the browser detect the format automatically. This is often the simplest and most maintainable approach.
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
<formaction=""method="post">
<labelfor="email">Email:</label>
<inputtype="email"id="email"name="email">
<buttontype="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:
<formmethod="post">
<labelfor="email">Email:</label>
<inputtype="email"id="email"name="email">
<buttontype="submit">Subscribe</button>
</form>
✅ Fixed: Provide a valid URL
If the form should submit to a specific endpoint, supply the URL:
<formaction="/subscribe"method="post">
<labelfor="email">Email:</label>
<inputtype="email"id="email"name="email">
<buttontype="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:
<formmethod="post"id="js-form">
<labelfor="query">Search:</label>
<inputtype="text"id="query"name="query">
<buttontype="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 -->
<formaction="/search?discount=20%off">
<buttontype="submit">Search</button>
</form>
Encode the % as %25:
<!-- ✅ Good: literal % encoded as %25 -->
<formaction="/search?discount=20%25off">
<buttontype="submit">Search</button>
</form>
Incomplete percent-encoding sequence
Here, %2 is missing its second hex digit:
<!-- ❌ Bad: %2 is an incomplete sequence -->
<formaction="/path/to%2file.html">
<buttontype="submit">Submit</button>
</form>
If the intent was to encode a / character (%2F), complete the sequence:
<!-- ✅ Good: complete percent-encoding for "/" -->
<formaction="/path/to%2Ffile.html">
<buttontype="submit">Submit</button>
</form>
Invalid hex characters after %
The characters G and Z are not hexadecimal digits:
<!-- ❌ Bad: %GZ is not valid hex -->
<formaction="/data%GZprocess">
<buttontype="submit">Go</button>
</form>
If %GZ was never intended as encoding, escape the % itself:
<!-- ✅ Good: literal % properly encoded -->
<formaction="/data%25GZprocess">
<buttontype="submit">Go</button>
</form>
Using JavaScript to encode safely
When building URLs dynamically, use built-in encoding functions to avoid this issue entirely:
constquery="20% off";
constsafeURL="/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 aria-activedescendant attribute tells assistive technologies which child element within a composite widget — such as a combobox, listbox, or autocomplete dropdown — is currently "active" or focused. Instead of moving actual DOM focus to each option, the parent element (like an input) retains focus while aria-activedescendant points to the visually highlighted option by referencing its id. This allows screen readers to announce the active option without disrupting keyboard interaction on the input.
When aria-activedescendant is set to an empty string (""), it creates an invalid state. The HTML and ARIA specifications require that any ID reference attribute either contains a valid, non-empty ID token or is omitted altogether. An empty string is not a valid ID, so the W3C validator flags this as an error: Bad value "" for attribute "aria-activedescendant" on element "input": An ID must not be the empty string.
This problem commonly occurs in JavaScript-driven widgets where aria-activedescendant is cleared by setting it to "" when no option is highlighted — for example, when a dropdown closes or the user clears their selection. While the developer's intent is correct (indicating that nothing is active), the implementation is wrong.
Why this matters
- Accessibility: Screen readers may behave unpredictably when encountering an empty ID reference. Some may silently ignore it, while others may announce errors or fail to convey widget state correctly.
- Standards compliance: The ARIA specification explicitly requires ID reference values to be non-empty strings that match an existing element's
id. - Browser consistency: Browsers handle invalid ARIA attributes inconsistently, which can lead to different experiences across platforms and assistive technologies.
How to fix it
- Remove the attribute when no descendant is active. Use
removeAttribute('aria-activedescendant')in JavaScript instead of setting it to an empty string. - Set a valid ID when a descendant becomes active, pointing to the
idof the currently highlighted or selected option. - Never render the attribute in HTML with an empty value. If your framework or templating engine conditionally renders attributes, ensure it omits the attribute entirely rather than outputting
aria-activedescendant="".
Examples
Incorrect: empty string value
This triggers the W3C validation error because the attribute value is an empty string.
<inputtype="text"role="combobox"aria-activedescendant=""/>
Correct: attribute omitted when no option is active
When nothing is active, simply leave the attribute off.
<inputtype="text"role="combobox"aria-expanded="false"/>
Correct: valid ID reference when an option is active
When a user highlights an option, set aria-activedescendant to that option's id.
<divrole="combobox">
<input
type="text"
role="combobox"
aria-expanded="true"
aria-controls="suggestions"
aria-activedescendant="option2"/>
<ulid="suggestions"role="listbox">
<liid="option1"role="option">Apple</li>
<liid="option2"role="option"aria-selected="true">Banana</li>
<liid="option3"role="option">Cherry</li>
</ul>
</div>
Correct: managing the attribute dynamically with JavaScript
The key fix in JavaScript is using removeAttribute instead of setting the value to an empty string.
<divrole="combobox">
<input
id="search"
type="text"
role="combobox"
aria-expanded="true"
aria-controls="results"/>
<ulid="results"role="listbox">
<liid="result1"role="option">First result</li>
<liid="result2"role="option">Second result</li>
</ul>
</div>
<script>
constinput=document.getElementById('search');
functionsetActiveOption(optionId){
if(optionId){
input.setAttribute('aria-activedescendant',optionId);
}else{
// Remove the attribute instead of setting it to ""
input.removeAttribute('aria-activedescendant');
}
}
</script>
In summary, always ensure aria-activedescendant either points to a real, non-empty id or is removed from the element. Never set it to an empty string.
The aria-controls attribute establishes a programmatic relationship between an interactive element (like a button or link) and the content it controls. Assistive technologies such as screen readers use this relationship to help users navigate between a control and the region it affects—for example, a button that toggles a panel or a tab that switches visible content. When the attribute is present but empty (aria-controls=""or aria-controls=" "), it signals a broken relationship: the browser and assistive technology expect a target element but find nothing.
This issue commonly occurs when aria-controls is added by a JavaScript framework or template before the target element's id is known, leaving behind an empty placeholder. It can also happen when a CMS or component library outputs the attribute unconditionally, even when no controlled region exists.
Why this matters
- Accessibility: Screen readers may announce that a control is associated with another element, but an empty reference leads nowhere. This creates a confusing or misleading experience for users who rely on assistive technology.
- Standards compliance: The HTML specification defines
aria-controlsas an IDREFS attribute, meaning its value must contain one or more space-separated tokens, each matching theidof an element in the same document. An empty value is invalid per both the WAI-ARIA specification and the HTML standard. - Maintainability: Empty or placeholder ARIA attributes are a sign of incomplete implementation. They can mask bugs in dynamic UIs where the controlled element was supposed to be rendered but wasn't, or where an
idwas accidentally removed during refactoring.
How to fix it
- If the element controls another region, set
aria-controlsto theidof that region. Make sure the target element exists in the DOM and has a uniqueid. - If the element doesn't control anything, remove the
aria-controlsattribute entirely. An absent attribute is always better than an empty one. - For dynamically rendered content, only add
aria-controlsafter the target element and itsidare present in the DOM. If using a framework, conditionally render the attribute. - Keep references in sync. If you rename or remove an
id, update everyaria-controlsthat references it.
Examples
Invalid: empty aria-controls
This triggers the validator error because the attribute value contains no id reference.
<ahref="#"aria-controls="">Toggle details</a>
Invalid: whitespace-only value
A value with only spaces is also invalid—IDREFS requires at least one non-whitespace token.
<buttontype="button"aria-controls="">Open menu</button>
Fixed: provide a valid id reference
Point aria-controls to the id of the element being controlled.
<buttontype="button"aria-controls="details-panel"aria-expanded="false">
Toggle details
</button>
<divid="details-panel"hidden>
<p>Here are some additional details.</p>
</div>
Fixed: controlling multiple regions
You can reference multiple id values separated by spaces. Each id must correspond to an element in the document.
<buttontype="button"aria-controls="filters results">
Show filters and results
</button>
<sectionid="filters"hidden>
<p>Filter options...</p>
</section>
<sectionid="results"hidden>
<p>Search results...</p>
</section>
Fixed: remove the attribute when not needed
If the element doesn't actually control another region, simply omit aria-controls.
<ahref="/details">View details</a>
Complete document with proper toggle behavior
This example shows a working toggle pattern combining aria-controls with aria-expanded for full accessibility.
<!doctype html>
<htmllang="en">
<head>
<metacharset="utf-8">
<title>aria-controls Toggle Example</title>
</head>
<body>
<buttontype="button"aria-controls="info-panel"aria-expanded="false">
Toggle info
</button>
<divid="info-panel"hidden>
<p>Extra information goes here.</p>
</div>
<script>
constbtn=document.querySelector('button');
constpanel=document.getElementById('info-panel');
btn.addEventListener('click',()=>{
constexpanded=btn.getAttribute('aria-expanded')==='true';
btn.setAttribute('aria-expanded',String(!expanded));
panel.hidden=expanded;
});
</script>
</body>
</html>
Conditional rendering in a framework
When using a templating system or JavaScript framework, only render aria-controls when the target id is available. Here's a conceptual example:
<!-- Good: only add the attribute when targetId has a value -->
<buttontype="button"aria-controls="sidebar-nav"aria-expanded="false">
Menu
</button>
<navid="sidebar-nav"hidden>
<ul>
<li><ahref="/">Home</a></li>
</ul>
</nav>
<!-- Bad: template outputs an empty value when targetId is undefined -->
<!-- <button type="button" aria-controls="">Menu</button> -->
In frameworks like React, you can conditionally spread the attribute: use aria-controls={targetId || undefined} so that when the value is empty, the attribute is omitted from the rendered HTML entirely rather than being set to an empty string.
The aria-controls attribute on a button element has an empty value, but it requires at least one valid ID reference.
The aria-controls attribute accepts a space-separated list of id values that point to the elements controlled by the current element. When a screen reader encounters a button with aria-controls, it can offer the user a way to navigate to the controlled element. An empty string ("") is not a valid IDREF, so the W3C validator rejects it.
This often happens when a template or JavaScript framework sets aria-controls before the target element's id is known, or when the value is conditionally generated and falls through to an empty string.
There are two ways to fix this: either set aria-controls to a valid id that exists in the document, or remove the attribute entirely when no target is available. An absent aria-controls attribute is perfectly fine and preferable to an empty one.
HTML examples
Invalid: empty aria-controls
<buttonaria-controls=""aria-expanded="false">
Toggle menu
</button>
<navid="main-menu"hidden>
<ahref="/about">About</a>
</nav>
Fixed: aria-controls references a valid ID
<buttonaria-controls="main-menu"aria-expanded="false">
Toggle menu
</button>
<navid="main-menu"hidden>
<ahref="/about">About</a>
</nav>
If the controlled element does not exist yet or the ID is not available, remove the attribute:
<buttonaria-expanded="false">
Toggle menu
</button>
JavaScript can add aria-controls later, once the target element and its id are present in the DOM.
The aria-controls attribute must contain at least one valid ID reference and cannot be an empty string.
The aria-controls attribute accepts a space-separated list of IDs pointing to elements that are controlled by the current element. When a screen reader encounters aria-controls, it can offer navigation to those referenced elements. An empty string is not a valid IDREF value per the WAI-ARIA specification, so the W3C validator rejects it.
If the element does not currently control anything, remove the aria-controls attribute entirely rather than leaving it empty. Setting it to an empty string does not mean "no value" — it means "invalid value," which can confuse assistive technologies.
If the controlled element is added dynamically (for example, a dropdown that appears after user interaction), add aria-controls through JavaScript at the same time the controlled element appears in the DOM, and remove it when the element is gone.
Examples
Invalid: empty aria-controls
<divaria-controls=""aria-expanded="false">
Toggle panel
</div>
Fixed: remove the attribute when unused
<divaria-expanded="false">
Toggle panel
</div>
Fixed: provide a valid ID when a controlled element exists
<divaria-controls="panel-1"aria-expanded="true">
Toggle panel
</div>
<divid="panel-1">
Panel content here.
</div>
The aria-describedby attribute cannot be an empty string. It must contain at least one valid ID reference, or be removed entirely.
The aria-describedby attribute accepts a space-separated list of id values that point to elements providing a description for the current element. When a screen reader encounters an element with aria-describedby, it reads the text content of each referenced element as additional context.
An empty string ("") is not a valid IDREF value. The W3C validator expects at least one non-whitespace character. This often happens when a template engine or JavaScript outputs an empty variable into the attribute, or when a CMS generates the attribute without a corresponding value.
If no description exists, remove the attribute altogether. An absent aria-describedby is perfectly fine — it simply means the element has no supplementary description. Assistive technologies handle missing attributes gracefully, but an empty one can cause unpredictable behavior.
Examples
Invalid: empty aria-describedby
<ahref="/settings"aria-describedby="">Account settings</a>
Fixed: attribute removed
<ahref="/settings">Account settings</a>
Fixed: attribute references a valid ID
<ahref="/settings"aria-describedby="settings-note">Account settings</a>
<pid="settings-note">Opens your profile and notification preferences.</p>
The aria-describedby attribute cannot be an empty string — it must either contain valid ID references or be removed entirely.
The aria-describedby attribute accepts one or more ID values (separated by spaces) that point to elements providing additional descriptive text for the current element. When a screen reader focuses on the element, it reads the content of the referenced elements to give the user more context.
Setting aria-describedby="" is invalid because the attribute expects at least one valid IDREF — a non-empty string that matches the id of another element in the page. An empty value doesn't reference anything and creates a validation error. If no description is needed, simply omit the attribute altogether.
This commonly happens when a template or JavaScript dynamically sets the attribute but provides an empty fallback value instead of removing the attribute entirely.
Invalid Example
<labelfor="email">Email</label>
<inputtype="email"id="email"aria-describedby="">
Fixed Examples
If there is no description to reference, remove the attribute:
<labelfor="email">Email</label>
<inputtype="email"id="email">
If a description exists, point to its id:
<labelfor="email">Email</label>
<inputtype="email"id="email"aria-describedby="email-hint">
<pid="email-hint">We'll never share your email with anyone.</p>
If you're generating the attribute dynamically, make sure your code removes aria-describedby entirely rather than setting it to an empty string when no hint is available.
The aria-labelledby attribute accepts an IDREFS value — a space-separated list of one or more id values that reference other elements in the document. The validator expects each ID in the list to be non-empty and contain at least one non-whitespace character. When the attribute is set to an empty string (aria-labelledby=""), it violates this constraint and triggers the validation error.
This issue commonly arises in templating systems and JavaScript frameworks where a variable intended to hold an ID reference resolves to an empty string. For example, a template like aria-labelledby="{{ labelId }}" will produce an empty attribute if labelId is undefined or blank.
Why this matters
The aria-labelledby attribute is one of the highest-priority methods for computing an element's accessible name. According to the accessible name computation algorithm, aria-labelledby overrides all other naming sources — including visible text content, aria-label, and the title attribute. When aria-labelledby is present but empty or broken, screen readers may calculate the link's accessible name as empty, effectively making the link invisible or meaningless to assistive technology users. A link with no accessible name is a significant accessibility barrier: users cannot determine where the link goes or what it does.
Beyond accessibility, an empty aria-labelledby also signals invalid HTML according to both the WHATWG HTML living standard and the WAI-ARIA specification, which define the IDREFS type as requiring at least one valid token.
How to fix it
You have several options depending on your situation:
- Reference a valid ID — Point
aria-labelledbyto theidof an existing element whose text content should serve as the link's accessible name. - Remove the attribute and use visible link text — If the link already contains descriptive text,
aria-labelledbyis unnecessary. - Use
aria-labelinstead — For icon-only links where no visible label element exists,aria-labelprovides a concise accessible name directly on the element. - Conditionally render the attribute — In templates, use conditional logic to omit
aria-labelledbyentirely when there's no valid ID to reference, rather than rendering an empty value.
Examples
Invalid: empty aria-labelledby
This triggers the validation error because the attribute value contains no non-whitespace characters.
<ahref="/report"aria-labelledby=""></a>
Invalid: whitespace-only aria-labelledby
A value containing only spaces is equally invalid — IDREFS requires at least one actual token.
<ahref="/report"aria-labelledby=""></a>
Fixed: referencing an existing element by id
The aria-labelledby attribute points to a <span> whose text content becomes the link's accessible name.
<ahref="/report"aria-labelledby="report-link-text">
<svgaria-hidden="true"viewBox="0 0 16 16"></svg>
</a>
<spanid="report-link-text">View report</span>
Fixed: referencing multiple IDs
You can concatenate text from multiple elements by listing their IDs separated by spaces. The accessible name is built by joining the referenced text in order.
<spanid="action">Learn more:</span>
<spanid="subject">Apples</span>
<ahref="/apples"aria-labelledby="action subject">
<svgaria-hidden="true"viewBox="0 0 16 16"></svg>
</a>
In this case, the computed accessible name is "Learn more: Apples".
Fixed: using visible link text instead
When the link already contains descriptive text, no ARIA attribute is needed. This is the simplest and most robust approach.
<ahref="/report">View report</a>
Fixed: using aria-label for an icon-only link
When there is no separate visible label element to reference, aria-label provides the accessible name directly.
<ahref="/search"aria-label="Search">
<svgaria-hidden="true"viewBox="0 0 16 16"></svg>
</a>
Fixed: conditional rendering in a template
If you're using a templating engine, conditionally include the attribute only when a value exists. The exact syntax varies by framework, but here's the general idea:
<!-- Instead of always rendering the attribute: -->
<!-- <a href="/report" aria-labelledby="{{ labelId }}"> -->
<!-- Only render it when labelId has a value: -->
<!-- <a href="/report" {{#if labelId}}aria-labelledby="{{labelId}}"{{/if}}> -->
This prevents the empty-attribute problem at its source rather than patching it after the fact.
The aria-labelledby attribute is an IDREFS attribute, meaning its value must be a space-separated list of one or more id values that exist in the document. These referenced elements collectively provide the accessible name for the element. When the value is an empty string ("") or contains only whitespace, there are no valid ID references, which violates the IDREFS requirement defined in the WAI-ARIA and HTML specifications.
This issue commonly appears when templating systems or JavaScript frameworks conditionally set aria-labelledby but output an empty string when no label ID is available. It also occurs when developers add the attribute as a placeholder with the intention of filling it in later but forget to do so.
Why this matters
An empty aria-labelledby is problematic for several reasons:
- Accessibility: Screen readers rely on
aria-labelledbyto announce the accessible name of an element. An empty value can cause unpredictable behavior — some screen readers may ignore the SVG entirely, while others may fall back to reading unhelpful content or nothing at all. This leaves users who depend on assistive technology without a meaningful description of the graphic. - Standards compliance: The W3C validator flags this as an error because the HTML specification requires IDREFS attributes to contain at least one non-whitespace character. Shipping invalid HTML can signal broader quality issues and may cause problems in strict parsing environments.
- Maintainability: An empty
aria-labelledbyis ambiguous. It's unclear whether the developer intended the SVG to be decorative, forgot to add a reference, or encountered a bug in their templating logic.
How to fix it
Choose the approach that matches your intent:
- Reference a labeling element by ID: If the SVG conveys meaning, add a
<title>element (or another visible text element) inside or near the SVG with a uniqueid, then setaria-labelledbyto thatid. IDs are case-sensitive, so ensure an exact match. - Use
aria-labelinstead: If you want to provide an accessible name directly as a text string without needing a separate element, replacearia-labelledbywitharia-label. - Remove the attribute: If the SVG already has an accessible name through other means (such as visible adjacent text or a
<title>child that doesn't need explicit referencing), simply remove the emptyaria-labelledby. - Mark as decorative: If the SVG is purely decorative and adds no information, remove
aria-labelledbyand addaria-hidden="true"so assistive technology skips it entirely.
When generating aria-labelledby dynamically, ensure your code omits the attribute entirely rather than outputting an empty value when no label ID is available.
Examples
❌ Empty aria-labelledby (triggers the error)
<svgrole="img"aria-labelledby="">
<usehref="#icon-star"></use>
</svg>
The empty string is not a valid IDREFS value, so the validator reports an error.
✅ Reference a <title> element by ID
<svgrole="img"aria-labelledby="star-title">
<titleid="star-title">Favorite</title>
<usehref="#icon-star"></use>
</svg>
The aria-labelledby points to the <title> element's id, giving the SVG a clear accessible name of "Favorite."
✅ Use aria-label with a text string
<svgrole="img"aria-label="Favorite">
<usehref="#icon-star"></use>
</svg>
When you don't need to reference another element, aria-label provides the accessible name directly as an attribute value.
✅ Reference multiple labeling elements
<svgrole="img"aria-labelledby="star-title star-desc">
<titleid="star-title">Favorite</title>
<descid="star-desc">A five-pointed star icon</desc>
<usehref="#icon-star"></use>
</svg>
The aria-labelledby value can include multiple space-separated IDs. The accessible name is constructed by concatenating the text content of the referenced elements in order.
✅ Decorative SVG (no accessible name needed)
<svgaria-hidden="true"focusable="false">
<usehref="#icon-decorative-divider"></use>
</svg>
For purely decorative graphics, aria-hidden="true" removes the element from the accessibility tree. Adding focusable="false" prevents the SVG from receiving keyboard focus in older versions of Internet Explorer and Edge.
The aria-valuenow attribute requires a valid numeric value and cannot be an empty string.
The aria-valuenow attribute is used on range-type widgets like progress bars, sliders, and spinbuttons to indicate the current value. It must be a valid floating point number — for example, 0, 50, or 3.5. An empty string ("") is not a number, so the validator rejects it.
If the current value is unknown or indeterminate, you should remove the aria-valuenow attribute entirely rather than setting it to an empty string. This is common with indeterminate progress bars where the completion percentage isn't known yet.
When present, aria-valuenow should fall between aria-valuemin and aria-valuemax. All three attributes work together to communicate the state of a range widget to assistive technologies.
Bad Example
<divrole="progressbar"
aria-valuenow=""
aria-valuemin="0"
aria-valuemax="100">
Loading...
</div>
Good Example — Known Value
<divrole="progressbar"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100">
50% complete
</div>
Good Example — Indeterminate State
<divrole="progressbar"
aria-valuemin="0"
aria-valuemax="100">
Loading...
</div>
In the indeterminate example, omitting aria-valuenow tells assistive technologies that the progress is ongoing but the exact value is not known.
The autocomplete attribute tells the browser whether and how it should autofill a form field. The HTML living standard defines a specific set of allowed values — an empty string is not among them. When the attribute is present but empty, the browser receives ambiguous instructions: you've explicitly declared the attribute, signaling intent, but provided no actual directive. Different browsers may interpret this inconsistently, some treating it as on, others ignoring it, and others falling back to default behavior.
This matters for several reasons:
- Standards compliance: The WHATWG HTML specification requires
autocompleteto contain either the keywordsonoroff, or one or more valid autofill detail tokens. An empty string satisfies none of these. - Accessibility: Autofill helps users with motor impairments or cognitive disabilities complete forms more quickly and accurately. An ambiguous
autocompletevalue can interfere with assistive technologies that rely on these hints. - User experience: Specific autofill tokens like
email,tel,street-address, andcurrent-passwordallow browsers and password managers to suggest the right data for the right field. Using them correctly makes forms faster and easier to complete.
This issue commonly arises when a framework or template engine outputs autocomplete="" as a default, or when a developer intends to disable autocomplete but leaves the value blank instead of using off.
How to fix it
Choose one of these approaches depending on your intent:
- Remove the attribute if you want default browser behavior (the browser decides whether to autofill).
- Use
onto explicitly allow autofill. - Use
offto explicitly discourage autofill (note: browsers may still autofill for login fields regardless). - Use a specific autofill token to tell the browser exactly what kind of data the field expects. This is the most helpful option for users.
Common autofill tokens include: name, given-name, family-name, email, username, new-password, current-password, tel, street-address, postal-code, country, and cc-number. You can find the full list in the WHATWG autofill specification.
Examples
Incorrect: empty autocomplete value
<inputtype="text"name="username"autocomplete="">
This triggers the validation error because the attribute is present but contains no valid token.
Correct: remove the attribute entirely
If you have no specific autofill preference, simply omit the attribute:
<inputtype="text"name="username">
Correct: use on or off
Explicitly enable or disable autofill:
<inputtype="text"name="username"autocomplete="on">
<inputtype="text"name="search-query"autocomplete="off">
Correct: use specific autofill tokens
Specific tokens give browsers the best hints for filling in the right data. This is the recommended approach for forms that collect personal information:
<form>
<labelfor="name">Full name</label>
<inputtype="text"id="name"name="name"autocomplete="name">
<labelfor="useremail">Email</label>
<inputtype="email"id="useremail"name="useremail"autocomplete="email">
<labelfor="phone">Phone</label>
<inputtype="tel"id="phone"name="phone"autocomplete="tel">
<labelfor="pwd">Password</label>
<inputtype="password"id="pwd"name="pwd"autocomplete="current-password">
<buttontype="submit">Sign in</button>
</form>
Using precise tokens like current-password and email helps password managers and mobile keyboards provide the most relevant suggestions, improving the experience for all users.
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.
Pro Trial
Full Pro access. Cancel anytime.
Start Pro Trial →Join teams across 40+ countries