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 automatically fill in form fields with previously saved user data. The HTML specification defines a strict set of valid autofill field names, and "company" is not among them. While "company" might seem like an intuitive choice, the spec uses "organization" to represent a company name, business name, or other organizational name associated with the person or address in the form.
Using an invalid autocomplete value means browsers won't recognize the field's purpose and cannot offer relevant autofill suggestions. This degrades the user experience — especially on mobile devices where autofill significantly speeds up form completion. It also impacts accessibility, as assistive technologies may rely on valid autocomplete tokens to help users understand and complete forms efficiently.
The full list of valid autofill field names is defined in the WHATWG HTML Living Standard. Some commonly used values include "name", "email", "tel", "street-address", "postal-code", "country", and "organization". When choosing a value, always refer to the specification rather than guessing a name that seems logical.
Examples
❌ Invalid: using "company" as an autocomplete value
<labelfor="company">Company Name</label>
<inputtype="text"id="company"name="company"autocomplete="company">
This triggers the validation error because "company" is not a recognized autofill field name.
✅ Valid: using "organization" instead
<labelfor="company">Company Name</label>
<inputtype="text"id="company"name="company"autocomplete="organization">
The value "organization" is the spec-defined autofill field name for "the company, organization, institution, or other entity associated with the person, address, or contact information in the other fields associated with this field."
✅ Valid: using "organization" with a section and purpose
You can combine "organization" with other valid tokens for more specificity:
<labelfor="work-org">Employer</label>
<inputtype="text"id="work-org"name="employer"autocomplete="section-work organization">
This tells the browser that the field is for an organization name within a specific named section of the form, which is useful when a form collects information about multiple entities.
Common Autofill Field Names for Business Forms
Here are some valid autocomplete values you might use alongside "organization" in a business-related form:
"organization"— company or organization name"organization-title"— job title (e.g., "Software Engineer", "CEO")"name"— full name of the contact person"email"— email address"tel"— telephone number"street-address"— full street address
Using the correct values ensures browsers can provide meaningful autofill suggestions, making your forms faster and easier to complete.
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-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 WAI-ARIA specification defines strict ownership requirements for certain roles. The listitem role is one such role — it must be "owned by" an element with role="list" or role="group". "Owned by" means the listitem must be either a direct DOM child of the owning element, or explicitly associated with it via the aria-owns attribute.
This matters because screen readers and other assistive technologies rely on the accessibility tree to convey structure to users. When a screen reader encounters a properly structured list, it announces something like "list, 3 items" and lets the user navigate between items. Without the parent role="list", the individual items lose their list context — users won't know how many items exist, where the list begins and ends, or that the items are related at all.
In most cases, the simplest and most robust fix is to use native HTML list elements (<ul> or <ol> with <li> children) instead of ARIA roles. Native elements have built-in semantics that don't require additional attributes. Only use ARIA roles when native elements aren't feasible — for example, when building a custom component where the visual layout prevents using standard list markup.
Examples
Incorrect: listitem without a parent list
These listitem elements are not contained within a role="list" or role="group" parent, so the validator reports an error.
<divrole="listitem">Apples</div>
<divrole="listitem">Bananas</div>
<divrole="listitem">Cherries</div>
Correct: wrapping items in role="list"
Adding a parent container with role="list" establishes the required ownership relationship.
<divrole="list">
<divrole="listitem">Apples</div>
<divrole="listitem">Bananas</div>
<divrole="listitem">Cherries</div>
</div>
Correct: using native HTML list elements
Native <ul> and <li> elements implicitly carry the list and listitem roles without any ARIA attributes. This is the preferred approach.
<ul>
<li>Apples</li>
<li>Bananas</li>
<li>Cherries</li>
</ul>
Correct: using role="group" for nested sublists
The role="group" container is appropriate for grouping a subset of list items within a larger list, such as a nested sublist.
<divrole="list">
<divrole="listitem">Fruits
<divrole="group">
<divrole="listitem">Apples</div>
<divrole="listitem">Bananas</div>
</div>
</div>
<divrole="listitem">Vegetables
<divrole="group">
<divrole="listitem">Carrots</div>
<divrole="listitem">Peas</div>
</div>
</div>
</div>
Note that role="group" should itself be nested inside a role="list" — it doesn't replace the top-level list container, but rather serves as an intermediate grouping mechanism within one.
Correct: using aria-owns for non-descendant ownership
If the DOM structure prevents you from nesting the items directly inside the list container, you can use aria-owns to establish the relationship programmatically.
<divrole="list"aria-owns="item-1 item-2 item-3"></div>
<!-- These items live elsewhere in the DOM -->
<divrole="listitem"id="item-1">Apples</div>
<divrole="listitem"id="item-2">Bananas</div>
<divrole="listitem"id="item-3">Cherries</div>
This approach should be used sparingly, as it can create confusion if the visual order doesn't match the accessibility tree order. Whenever possible, restructure your HTML so the items are actual descendants of the list container.
The HTML5 specification mandates UTF-8 as the only permitted character encoding for web documents declared via <meta> tags. Legacy encodings such as windows-1251 (a Cyrillic character encoding), iso-8859-1, shift_jis, and others are no longer valid values in HTML5 <meta> declarations. This restriction exists because UTF-8 is a universal encoding that can represent virtually every character from every writing system, eliminating the interoperability problems that plagued the web when dozens of competing encodings were in use.
When the validator encounters content="text/html; charset=windows-1251" on a <meta> element, it flags it because the charset= portion must be followed by utf-8 — no other value is accepted. This applies whether you use the longer <meta http-equiv="Content-Type"> syntax or the shorter <meta charset> syntax.
Why this matters
- Standards compliance: The WHATWG HTML Living Standard explicitly requires
utf-8as the character encoding when declared in a<meta>tag. Non-conforming encodings will trigger validation errors. - Internationalization: UTF-8 supports all Unicode characters, making your pages work correctly for users across all languages, including Cyrillic text that
windows-1251was originally designed for. - Security: Legacy encodings can introduce security vulnerabilities, including certain cross-site scripting (XSS) attack vectors that exploit encoding ambiguity.
- Browser consistency: While browsers may still recognize legacy encodings, relying on them can cause mojibake (garbled text) when there's a mismatch between the declared and actual encoding.
How to fix it
- Update the
<meta>tag to declareutf-8as the charset. - Re-save your file in UTF-8 encoding using your text editor or IDE. Most modern editors support this — look for an encoding option in "Save As" or in the status bar.
- Verify your server configuration. If your server sends a
Content-TypeHTTP header with a different encoding, the server header takes precedence over the<meta>tag. Make sure both agree on UTF-8. - Convert your content. If your text was originally written in
windows-1251, you may need to convert it to UTF-8. Tools likeiconvon the command line can help:iconv -f WINDOWS-1251 -t UTF-8 input.html > output.html.
Examples
❌ Incorrect: Using windows-1251 charset
<metahttp-equiv="Content-Type"content="text/html; charset=windows-1251">
✅ Correct: Using utf-8 with http-equiv syntax
<metahttp-equiv="Content-Type"content="text/html; charset=utf-8">
✅ Correct: Using the shorter <meta charset> syntax (preferred in HTML5)
<metacharset="utf-8">
Full document example
<!DOCTYPE html>
<htmllang="ru">
<head>
<metacharset="utf-8">
<title>Пример страницы</title>
</head>
<body>
<p>Текст на русском языке в кодировке UTF-8.</p>
</body>
</html>
The shorter <meta charset="utf-8"> syntax is generally preferred in HTML5 documents because it's more concise and achieves the same result. Whichever syntax you choose, place the charset declaration within the first 1024 bytes of your document — ideally as the very first element inside <head> — so browsers can detect the encoding as early as possible.
XML processing instructions are a feature of XML, not HTML. They begin with <? and end with ?>, and are used in XML documents to carry instructions for applications processing the document. The most common example is the XML declaration: <?xml version="1.0" encoding="UTF-8"?>. While these are perfectly valid in XML, the HTML parser does not recognize them. When the parser encounters <?, it doesn't know how to handle it and treats it as a bogus comment, which leads to unexpected behavior and this validation error.
This matters for several reasons. First, standards compliance — HTML5 has a clearly defined parsing algorithm, and processing instructions are not part of it. Second, browser behavior becomes unpredictable — different browsers may handle the unexpected <? content differently, potentially exposing raw code or breaking your layout. Third, if server-side code like PHP leaks into the output, it can expose sensitive logic or configuration details to end users.
There are three common causes of this error:
1. Inlining SVG files with their XML declaration. When you copy the contents of an .svg file and paste it directly into HTML, the file often starts with an XML declaration and possibly a <?xml-stylesheet?> processing instruction. These must be removed — only the <svg> element and its children should be included.
2. Unprocessed server-side code. Languages like PHP use <?php ... ?> (or the short tag <? ... ?>) syntax. If the server isn't configured to process PHP files, or if a .html file contains PHP code without being routed through the PHP interpreter, the raw <?php tags end up in the HTML sent to the browser.
3. Copy-pasting XML content. Other XML-based formats (like RSS feeds, XHTML fragments, or MathML with XML declarations) may include processing instructions that aren't valid in HTML.
How to Fix It
- For inline SVGs: Open the SVG file in a text editor and copy only the
<svg>...</svg>element. Remove the<?xml ...?>declaration and any other processing instructions that precede the<svg>tag. - For PHP or other server-side code: Ensure your server is properly configured to process the files. Check that the file extension matches what the server expects (e.g.,
.phpfor PHP files). Verify that the server-side language is installed and running. - For other XML content: Strip out any
<?...?>processing instructions before embedding the content in HTML.
Examples
❌ Inline SVG with XML declaration
<body>
<?xml version="1.0" encoding="UTF-8"?>
<svgxmlns="http://www.w3.org/2000/svg"viewBox="0 0 100 100">
<circlecx="50"cy="50"r="40"fill="blue"/>
</svg>
</body>
The <?xml version="1.0" encoding="UTF-8"?> line triggers the error.
✅ Inline SVG without XML declaration
<body>
<svgxmlns="http://www.w3.org/2000/svg"viewBox="0 0 100 100">
<circlecx="50"cy="50"r="40"fill="blue"/>
</svg>
</body>
❌ Unprocessed PHP code in HTML output
<body>
<h1>Welcome</h1>
<p><?php echo "Hello, World!"; ?></p>
</body>
If the server doesn't process the PHP, the raw <?php ... ?> ends up in the HTML and triggers this error.
✅ Properly processed output (or static HTML equivalent)
<body>
<h1>Welcome</h1>
<p>Hello, World!</p>
</body>
❌ SVG with an XML stylesheet processing instruction
<body>
<?xml-stylesheet type="text/css" href="style.css"?>
<svgxmlns="http://www.w3.org/2000/svg"viewBox="0 0 50 50">
<rectwidth="50"height="50"fill="red"/>
</svg>
</body>
✅ SVG without the processing instruction
<body>
<svgxmlns="http://www.w3.org/2000/svg"viewBox="0 0 50 50">
<rectwidth="50"height="50"fill="red"/>
</svg>
</body>
In HTML, use standard <link> or <style> elements in the <head> for stylesheets instead of XML processing instructions.
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 WAI-ARIA specification defines strict ownership requirements for certain roles. The menuitem role represents an option in a set of choices and is only meaningful when it exists within the context of a menu. When a menuitem appears outside of a menu or menubar, screen readers and other assistive technologies have no way to determine that it belongs to a menu widget. They cannot announce the total number of items, provide keyboard navigation between items, or convey the menu's hierarchical structure to the user.
This requirement follows the concept of required owned elements and required context roles in ARIA. Just as a <li> element belongs inside a <ul> or <ol>, a menuitem belongs inside a menu or menubar. The relationship can be established in two ways:
- DOM nesting — the
menuitemelement is a DOM descendant of themenuormenubarelement. - The
aria-ownsattribute — themenuormenubarelement usesaria-ownsto reference themenuitemby itsid, establishing ownership even when the elements aren't nested in the DOM.
It's important to note that ARIA menu roles are intended for application-style menus — the kind you'd find in a desktop application (e.g., File, Edit, View menus). They are not meant for standard website navigation. For typical site navigation, use semantic HTML elements like <nav> with <ul>, <li>, and <a> elements instead.
How to fix it
- Identify every element with
role="menuitem"in your markup. - Ensure each one is contained within an element that has
role="menu"orrole="menubar", either through DOM nesting or viaaria-owns. - Choose the correct parent role:
- Use
role="menubar"for a persistent, typically horizontal menu bar (like a desktop application's top-level menu). - Use
role="menu"for a popup or dropdown menu that contains a group of menu items.
- Use
- If you're using menus for site navigation, consider removing the ARIA menu roles entirely and using semantic HTML (
<nav>,<ul>,<li>,<a>) instead.
Examples
Incorrect — menuitem without a menu context
This triggers the validator error because the menuitem elements have no parent menu or menubar:
<div>
<divrole="menuitem">Cut</div>
<divrole="menuitem">Copy</div>
<divrole="menuitem">Paste</div>
</div>
Correct — menuitem inside a menu
Wrapping the items in an element with role="menu" resolves the issue:
<divrole="menu">
<divrole="menuitem"tabindex="0">Cut</div>
<divrole="menuitem"tabindex="-1">Copy</div>
<divrole="menuitem"tabindex="-1">Paste</div>
</div>
Correct — menuitem inside a menubar
For a persistent horizontal menu bar with application-style actions:
<divrole="menubar">
<divrole="menuitem"tabindex="0">File</div>
<divrole="menuitem"tabindex="-1">Edit</div>
<divrole="menuitem"tabindex="-1">View</div>
</div>
Correct — nested menus with dropdown submenus
A menubar with a dropdown menu containing additional menuitem elements:
<divrole="menubar">
<divrole="menuitem"tabindex="0"aria-haspopup="true"aria-expanded="false">
File
<divrole="menu">
<divrole="menuitem"tabindex="-1">New</div>
<divrole="menuitem"tabindex="-1">Open</div>
<divrole="menuitem"tabindex="-1">Save</div>
</div>
</div>
<divrole="menuitem"tabindex="-1"aria-haspopup="true"aria-expanded="false">
Edit
<divrole="menu">
<divrole="menuitem"tabindex="-1">Cut</div>
<divrole="menuitem"tabindex="-1">Copy</div>
<divrole="menuitem"tabindex="-1">Paste</div>
</div>
</div>
</div>
Correct — using aria-owns for ownership without DOM nesting
When the menuitem elements cannot be nested inside the menu in the DOM (e.g., due to layout constraints), use aria-owns to establish the relationship:
<divrole="menu"aria-owns="item-cut item-copy item-paste"></div>
<divrole="menuitem"id="item-cut"tabindex="0">Cut</div>
<divrole="menuitem"id="item-copy"tabindex="-1">Copy</div>
<divrole="menuitem"id="item-paste"tabindex="-1">Paste</div>
Better alternative — use semantic HTML for site navigation
If you're building standard website navigation (not an application-style menu), avoid ARIA menu roles altogether:
<navaria-label="Main navigation">
<ul>
<li><ahref="/">Home</a></li>
<li><ahref="/about">About</a></li>
<li><ahref="/contact">Contact</a></li>
</ul>
</nav>
This approach is simpler, more accessible by default, and doesn't trigger the validator warning. Reserve role="menu", role="menubar", and role="menuitem" for true application-style menus that implement full keyboard interaction patterns as described in the ARIA Authoring Practices Guide.
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 <label> element associates descriptive text with a specific form control, enabling users to click the label to focus or activate the associated input. The for attribute creates this link by referencing the id of the target form control. When the referenced id doesn't correspond to a valid, non-hidden form control, the label becomes orphaned — it isn't associated with anything meaningful.
The W3C validator raises this error in several scenarios:
- The
forattribute references anidthat doesn't exist in the document. - The
forattribute references an element that isn't a labelable element (such as a<div>or<span>). - The
forattribute references an<input type="hidden">, which is not a visible form control and cannot be labeled. - There's a typo or mismatch between the
forvalue and the intended element'sid.
Labelable elements in HTML include <input> (except type="hidden"), <select>, <textarea>, <button>, <meter>, <output>, and <progress>.
This matters for accessibility because screen readers rely on the for/id association to announce what each form control represents. Without a valid association, users who depend on assistive technology may not understand what a form field is asking for. It also impacts usability — a properly linked label expands the clickable area for the form control, making it easier to interact with, especially on touch devices and for users with motor impairments.
To fix this issue, verify that the for attribute value exactly matches the id of a visible, labelable form control. If you're labeling a hidden input, consider whether the label is necessary at all (hidden inputs are not user-facing). If the target element isn't a form control, either change it to the appropriate form element or use a different approach like aria-labelledby.
Examples
❌ for references a non-existent ID
<labelfor="username">Username</label>
<inputid="user-name"type="text">
The for value "username" doesn't match the input's id of "user-name".
✅ Fixed: matching for and id
<labelfor="username">Username</label>
<inputid="username"type="text">
❌ for references a hidden input
<labelfor="token">Token</label>
<inputid="token"type="hidden"value="abc123">
An <input type="hidden"> is not a visible form control and cannot be labeled.
✅ Fixed: remove the unnecessary label
<inputid="token"type="hidden"value="abc123">
Hidden inputs don't need labels since users never interact with them directly.
❌ for references a non-labelable element
<labelfor="info">Information</label>
<divid="info">Some details here</div>
A <div> is not a labelable element, so the for association is invalid.
✅ Fixed: use a proper form control or implicit labeling
<labelfor="info">Information</label>
<textareaid="info"></textarea>
❌ for references an element inside another form control
<labelfor="opt">Choose one</label>
<select>
<optionid="opt"value="a">Option A</option>
</select>
An <option> element is not a labelable element. The label should point to the <select>.
✅ Fixed: reference the <select> element
<labelfor="choice">Choose one</label>
<selectid="choice">
<optionvalue="a">Option A</option>
</select>
Using implicit labeling as an alternative
Instead of using the for attribute, you can wrap the form control inside the <label> element. This creates an implicit association without needing for or id at all:
<label>
Age
<inputtype="number">
</label>
This approach avoids the for/id mismatch problem entirely and is equally valid for accessibility.
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 srcset attribute lets you provide multiple image sources so the browser can select the most appropriate one. There are two types of descriptors you can use in srcset: pixel density descriptors (e.g., 1x, 2x) and width descriptors (e.g., 400w, 800w). When you use pixel density descriptors, the browser already knows the relationship between each source — it simply picks the one matching the device's pixel ratio. But width descriptors work differently. They tell the browser the intrinsic pixel width of each image file, and the browser then needs to know how wide the image will actually be rendered on screen to calculate which file is the best fit. That's exactly what the sizes attribute provides.
The sizes attribute accepts a comma-separated list of media conditions paired with length values, plus a default length. For example, sizes="(max-width: 600px) 100vw, 50vw" tells the browser: "If the viewport is 600px wide or less, this image will occupy 100% of the viewport width; otherwise, it will occupy 50%." Armed with this information and the width descriptors in srcset, the browser can do the math and download only the most suitable image — before CSS or layout has even been calculated.
Why this matters
- Standards compliance: The HTML specification requires
sizeswheneversrcsetuses width descriptors. Omitting it produces invalid HTML. - Correct image selection: Without
sizes, browsers fall back to assuming the image will be100vwwide, which often leads to downloading unnecessarily large images on desktop layouts where the image is much smaller than the full viewport. - Performance: Serving oversized images wastes bandwidth and slows page load. A proper
sizesattribute ensures the browser downloads the smallest sufficient image. - Predictable behavior: Relying on the browser's fallback assumption (
100vw) makes your responsive images behave inconsistently and defeats the purpose of providing multiple candidates.
How to fix it
- Identify every
img(orsource) element that uses width descriptors insrcset. - Determine how wide the image will be displayed at different viewport sizes. You can inspect this with your browser's developer tools or by reviewing your CSS.
- Add a
sizesattribute that describes those widths using media conditions and CSS length values likepx,vw,em, orcalc()expressions.
Examples
Incorrect — missing sizes with width descriptors
<img
src="photo-400.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w"
alt="A mountain landscape">
This triggers the validation error because the browser sees width descriptors (400w, 800w, 1200w) but has no sizes attribute to determine the image's rendered width.
Correct — sizes attribute added
<img
src="photo-400.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, (max-width: 1000px) 50vw, 600px"
alt="A mountain landscape">
Here, the sizes attribute tells the browser:
- On viewports up to 600px wide, the image fills 100% of the viewport.
- On viewports between 601px and 1000px, the image fills 50% of the viewport.
- On larger viewports, the image is displayed at a fixed 600px width.
Correct — pixel density descriptors (no sizes needed)
<img
src="logo-1x.png"
srcset="logo-1x.png 1x, logo-2x.png 2x"
alt="Company logo">
When using pixel density descriptors (1x, 2x) instead of width descriptors, the sizes attribute is not required. The browser simply matches the descriptor to the device's pixel ratio.
Correct — using sizes with a <picture> element
<picture>
<source
srcset="hero-400.webp 400w, hero-800.webp 800w"
sizes="(max-width: 800px) 100vw, 800px"
type="image/webp">
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w"
sizes="(max-width: 800px) 100vw, 800px"
alt="Hero banner">
</picture>
The sizes attribute is required on both the source and img elements when either uses width descriptors in its srcset.
The HTML specification mandates that documents must be encoded in UTF-8. This requirement exists because UTF-8 is the universal character encoding that supports virtually every character from every writing system in the world. Older encodings like windows-1252, iso-8859-1, or shift_jis only support a limited subset of characters and can cause text to display incorrectly — showing garbled characters or question marks — especially for users in different locales or when content includes special symbols, accented letters, or emoji.
When the validator encounters charset=windows-1252 in your <meta> tag, it flags this as an error because the HTML living standard (WHATWG) explicitly states that the character encoding declaration must specify utf-8 as the encoding. This isn't just a stylistic preference — browsers and other tools rely on this declaration to correctly interpret the bytes in your document. Using a non-UTF-8 encoding can lead to security vulnerabilities (such as encoding-based XSS attacks) and accessibility issues when assistive technologies misinterpret characters.
To fix this issue, take two steps:
- Update the
<meta>tag to declareutf-8as the charset. - Re-save your file with UTF-8 encoding. Most modern code editors (VS Code, Sublime Text, etc.) let you choose the encoding when saving — look for an option like "Save with Encoding" or check the status bar for the current encoding. If your file was originally in
windows-1252, simply changing the<meta>tag without re-encoding the file could cause existing special characters to display incorrectly.
The HTML spec also recommends using the shorter <meta charset="utf-8"> form rather than the longer <meta http-equiv="Content-Type" ...> pragma directive, as it's simpler and achieves the same result. Either form is valid, but the charset declaration must appear within the first 1024 bytes of the document.
Examples
Incorrect: Using windows-1252 charset
<metahttp-equiv="Content-Type"content="text/html; charset=windows-1252">
This triggers the validator error because the charset is not utf-8.
Correct: Using the short charset declaration (recommended)
<metacharset="utf-8">
Correct: Using the http-equiv pragma directive with utf-8
<metahttp-equiv="Content-Type"content="text/html; charset=utf-8">
Full document example
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="utf-8">
<title>My Page</title>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
Note that the <meta charset="utf-8"> tag should be the first element inside <head>, before any other elements (including <title>), so the browser knows the encoding before it starts parsing the rest of the document.
The label element associates a caption with a form control. There are two ways to create this association:
- Implicit association — Place the form control directly inside the
labelelement. Nofororidattributes are needed. - Explicit association — Use the
forattribute on thelabel, setting its value to theidof the associated form control.
Both methods work independently. The problem arises when you mix them incorrectly — nesting a select inside a label that has a for attribute, but the select either has no id or has an id that doesn't match the for value. In this situation, the explicit association (via for) points to nothing or to the wrong element, while the implicit association (via nesting) still exists. This contradictory state violates the HTML specification and can cause assistive technologies like screen readers to misidentify or skip the label, reducing accessibility for users who rely on them.
The WHATWG HTML specification requires that when a for attribute is present on a label, it must reference a valid labelable element by id. If the form control is already nested inside the label, the for attribute is redundant — but if present, it still must correctly reference that control.
How to Fix
You have two options:
- Remove the
forattribute — If theselectis already inside thelabel, implicit association handles everything. This is the simplest fix. - Add or correct the
id— Keep theforattribute but ensure theselecthas a matchingid.
Examples
❌ Incorrect: for attribute with no matching id
The for attribute references "age", but the select has no id at all:
<labelfor="age">
Age
<select>
<option>Young</option>
<option>Old</option>
</select>
</label>
❌ Incorrect: for attribute with a mismatched id
The for attribute references "age", but the select has a different id:
<labelfor="age">
Age
<selectid="age-select">
<option>Young</option>
<option>Old</option>
</select>
</label>
✅ Fix option 1: Remove the for attribute (implicit association)
Since the select is nested inside the label, implicit association is sufficient:
<label>
Age
<select>
<option>Young</option>
<option>Old</option>
</select>
</label>
✅ Fix option 2: Match the for attribute with the id (explicit association)
Keep the for attribute and give the select a matching id:
<labelfor="age">
Age
<selectid="age">
<option>Young</option>
<option>Old</option>
</select>
</label>
✅ Fix option 3: Separate the label and select (explicit association only)
If you prefer to keep the select outside the label, explicit association with matching for and id is required:
<labelfor="age">Age</label>
<selectid="age">
<option>Young</option>
<option>Old</option>
</select>
In most cases, option 1 (removing the for attribute) is the cleanest solution when the control is already nested. Use explicit association when the label and control are in separate parts of the DOM, such as in complex table or grid layouts.
The as attribute specifies the type of content a <link> element is fetching — such as "style", "script", "font", or "image". The browser uses this information to set the correct request headers, apply the right Content Security Policy, and assign the appropriate priority to the request. However, the HTML specification restricts the as attribute to <link> elements whose rel attribute is either "preload" or "modulepreload". Using as with any other rel value (like "stylesheet", "icon", or a missing rel altogether) is invalid HTML.
This validation error commonly occurs in two scenarios:
- You intended to preload a resource but forgot to set
rel="preload"— for example, writing<link href="styles.css" as="style">without specifyingrel. - You added
asto a regular<link>by mistake — for example, writing<link rel="stylesheet" href="styles.css" as="style">, whereasis unnecessary becauserel="stylesheet"already tells the browser what type of resource it is.
Getting this right matters for several reasons. Browsers rely on valid rel values to determine how to handle linked resources. An incorrect combination may cause the browser to ignore the as attribute entirely, leading to double-fetching of resources or incorrect prioritization. Additionally, invalid HTML can cause unpredictable behavior across different browsers.
How to fix it
- If you want to preload a resource (font, stylesheet, image, script, etc.), set
rel="preload"and keep theasattribute. - If you want to preload a JavaScript module, set
rel="modulepreload". Theasattribute defaults to"script"for module preloads and is optional in that case. - If you're using a different
relvalue like"stylesheet"or"icon", remove theasattribute — it's not needed and not allowed.
Examples
Incorrect: as attribute without rel="preload"
This <link> has as="style" but no rel attribute:
<linkhref="styles.css"as="style">
Incorrect: as attribute with rel="stylesheet"
The as attribute is not valid on a stylesheet link:
<linkrel="stylesheet"href="styles.css"as="style">
Correct: preloading a stylesheet
Use rel="preload" with the as attribute to hint the resource type:
<linkrel="preload"href="styles.css"as="style">
Note that preloading a stylesheet doesn't apply it — you still need a separate <link rel="stylesheet"> to actually use the CSS.
Correct: preloading a font
<linkrel="preload"href="font.woff2"as="font"type="font/woff2"crossorigin>
The crossorigin attribute is required when preloading fonts, even if they're served from the same origin.
Correct: preloading a JavaScript module
<linkrel="modulepreload"href="app.js">
With rel="modulepreload", the as attribute defaults to "script", so you can omit it. You may still include it explicitly if you prefer:
<linkrel="modulepreload"href="app.js"as="script">
Correct: regular stylesheet (no as needed)
If you simply want to load a stylesheet, no as attribute is required:
<linkrel="stylesheet"href="styles.css">
Full document example
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>Preload Example</title>
<!-- Preload critical resources -->
<linkrel="preload"href="styles.css"as="style">
<linkrel="preload"href="hero.jpg"as="image">
<linkrel="preload"href="font.woff2"as="font"type="font/woff2"crossorigin>
<!-- Apply the stylesheet -->
<linkrel="stylesheet"href="styles.css">
</head>
<body>
<h1>Hello, World!</h1>
<imgsrc="hero.jpg"alt="Hero banner">
</body>
</html>
The label element represents a caption for a form control. There are two ways to associate a label with its control:
- Implicit association — Place the form control directly inside the
labelelement. Noforattribute is needed because the browser automatically pairs the label with the nested control. - Explicit association — Use the
forattribute on thelabel, setting its value to theidof the target form control. The control doesn't need to be nested inside thelabelin this case.
Both methods are valid on their own. The problem occurs when you combine them incorrectly: you nest an input inside a label that has a for attribute, but the input either has no id or has an id that doesn't match the for value. This creates a contradiction — the for attribute points to a specific id, yet the nested input doesn't fulfill that reference. Browsers may handle this inconsistently, and assistive technologies like screen readers could fail to announce the label correctly, harming accessibility.
Why this matters
- Accessibility: Screen readers rely on the
for/idpairing to announce labels for form controls. A mismatched or missingidcan leave the control unlabeled for users who depend on assistive technology. - Standards compliance: The HTML specification requires that when a
forattribute is present, it must reference theidof a labelable element. A mismatch violates this rule. - Browser behavior: Some browsers will fall back to the implicit association when
fordoesn't resolve, but others may prioritize the broken explicit association, leaving the control effectively unlabeled.
How to fix it
You have two options:
- Remove the
forattribute if theinputis already nested inside thelabel. The implicit association is sufficient on its own. - Add or correct the
idon the nestedinputso it matches theforattribute value exactly.
Examples
❌ Nested input with no matching id
The for attribute says "email", but the input has no id at all:
<labelfor="email">
<inputtype="email"name="email">
</label>
❌ Nested input with a mismatched id
The for attribute says "email", but the input's id is "user-email":
<labelfor="email">
<inputtype="email"name="email"id="user-email">
</label>
✅ Fix by removing the for attribute (implicit association)
Since the input is nested inside the label, the association is automatic:
<label>
<inputtype="email"name="email">
</label>
✅ Fix by adding a matching id (explicit association)
The for value and the id value are identical:
<labelfor="email">
<inputtype="email"name="email"id="email">
</label>
✅ Fix by using explicit association without nesting
If you prefer to keep the for attribute, the input doesn't need to be nested at all:
<labelfor="email">Email</label>
<inputtype="email"name="email"id="email">
In most cases, choosing either implicit or explicit association — rather than mixing both — is the simplest way to avoid this error. If you do combine them, just make sure the for value and the id value match exactly.
The core problem is that aria-disabled="true" is purely an accessibility hint — it communicates a disabled state to assistive technologies like screen readers, but it has no effect on the actual behavior of the element. When an a element has an href attribute, the browser treats it as a valid hyperlink regardless of any ARIA attributes. Users can still click it, follow it via keyboard navigation, and navigate to its destination. This mismatch between the announced state ("disabled") and actual behavior ("fully functional link") creates a confusing and misleading experience, particularly for users of assistive technologies.
The W3C validator flags this combination because it violates the principle that ARIA states should accurately reflect an element's true interactive state. A link that claims to be disabled but still works undermines user trust and can cause real usability problems.
Why this matters
- Accessibility: Screen readers will announce the link as disabled, but users who activate it will be unexpectedly navigated away. This is disorienting and violates WCAG guidance on predictable behavior.
- Standards compliance: The HTML specification and ARIA in HTML requirements discourage or disallow this combination because it produces an unreliable user experience.
- Browser behavior: No browser will disable a link just because
aria-disabled="true"is present. Thehrefattribute always makes theaelement an active hyperlink.
How to fix it
You have two main approaches depending on your intent:
The link should be active: Remove
aria-disabled="true"and keep thehref. If the link works, don't mark it as disabled.The link should be disabled: Remove the
hrefattribute. Withouthref, theaelement becomes a placeholder link that is not interactive. You can then usearia-disabled="true"to communicate the disabled state,tabindex="-1"to remove it from the keyboard tab order, and CSS to style it as visually disabled. You should also add JavaScript to prevent activation if needed.
Examples
Incorrect
This triggers the validation error because aria-disabled="true" conflicts with the presence of href:
<ahref="/dashboard"aria-disabled="true">Go to Dashboard</a>
Correct — Keep the link active
If the link should function normally, simply remove the aria-disabled attribute:
<ahref="/dashboard">Go to Dashboard</a>
Correct — Disable the link
If the link should be non-actionable (e.g., a navigation item the user doesn't currently have access to), remove the href attribute and use ARIA and CSS to communicate the disabled state:
<aaria-disabled="true"tabindex="-1"role="link"class="link-disabled">Go to Dashboard</a>
.link-disabled{
color:#6c757d;
cursor: not-allowed;
pointer-events: none;
text-decoration: none;
}
In this approach:
- Removing
hrefensures the link is not actionable by the browser. aria-disabled="true"tells assistive technologies the element is disabled.tabindex="-1"removes the element from the keyboard tab order so users can't Tab to it.role="link"preserves the link semantics so screen readers still identify it as a link (anawithouthrefloses its implicit link role).- The CSS provides a visual indication that the element is disabled, with
pointer-events: nonepreventing mouse clicks andcursor: not-allowedgiving a visual cue on hover.
Correct — Use a button instead
If the "link" triggers an action rather than navigating somewhere, consider using a button element instead. Buttons natively support the disabled attribute:
<buttontype="button"disabled>Perform Action</button>
This is the simplest and most robust solution when the element doesn't need to be a link. The disabled attribute is natively understood by browsers and assistive technologies without any ARIA workarounds.
Unicode allows certain characters — especially accented letters and other composed characters — to be represented in multiple ways. For example, the letter "é" can be a single precomposed character (U+00E9, NFC form) or a base letter "e" (U+0065) followed by a combining acute accent (U+0301, NFD form). While they may look identical on screen, they are different byte sequences. The HTML specification requires that all attribute values use NFC to ensure consistent behavior across browsers, search engines, and assistive technologies.
This matters for several important reasons:
- String matching and comparison: Browsers and scripts may compare attribute values byte-by-byte. An
idvalue in NFD form won't match a CSS selector or fragment identifier targeting the NFC form, causing broken links and broken styles. - Accessibility: Screen readers and other assistive technologies may process NFC and NFD strings differently, potentially mispronouncing text or failing to match ARIA references.
- Interoperability: Different operating systems produce different normalization forms by default (macOS file systems historically use NFD, for example). Copying text from various sources can introduce non-NFC characters without any visual indication.
- Standards compliance: The WHATWG HTML specification and W3C guidance on normalization explicitly recommend NFC for all HTML content.
The issue most commonly appears when attribute values contain accented characters (like in id, class, alt, title, or value attributes) that were copied from a source using NFD normalization, or when files are created on systems that default to NFD.
To fix the problem, you need to convert the affected attribute values to NFC. You can do this by:
- Retyping the characters directly in your editor, which usually produces NFC by default.
- Using a programming tool such as Python's
unicodedata.normalize('NFC', text), JavaScript'stext.normalize('NFC'), or similar utilities in your language of choice. - Using a text editor that supports normalization conversion (some editors have built-in Unicode normalization features or plugins).
- Running a batch conversion on your HTML files before deployment as part of your build process.
Examples
Incorrect: Attribute value uses NFD (decomposed form)
In this example, the id attribute value for "résumé" uses decomposed characters (base letter + combining accent), which triggers the validation error. The decomposition is invisible in source code but present at the byte level.
<!-- The "é" here is stored as "e" + combining acute accent (NFD) -->
<divid="résumé">
<p>My résumé content</p>
</div>
Correct: Attribute value uses NFC (precomposed form)
Here, the id attribute value uses precomposed characters, which is the correct NFC form.
<!-- The "é" here is stored as a single precomposed character (NFC) -->
<divid="résumé">
<p>My résumé content</p>
</div>
While these two examples look identical in source view, they differ at the byte level. You can verify the normalization form using browser developer tools or a hex editor.
Checking and fixing with JavaScript
You can programmatically normalize attribute values:
<script>
// Check if a string is in NFC
consttext="résumé";
constnfcText=text.normalize("NFC");
console.log(text===nfcText);// false if original was NFD
</script>
Checking and fixing with Python
importunicodedata
text="r\u0065\u0301sume\u0301"# NFD form
normalized=unicodedata.normalize('NFC', text)
print(normalized) # Outputs NFC form: "résumé"
If you encounter this validation error, inspect the flagged attribute value carefully and ensure all characters are in their precomposed NFC form. Adding a normalization step to your build pipeline is a reliable way to prevent this issue from recurring.
The HTML specification defines specific rules about which autocomplete autofill field names can be paired with which input types. The tel-national token (which represents a phone number without the country code) is classified as requiring a text-based input control. Meanwhile, <input type="tel"> is a specialized control that the spec treats differently from a plain text field. When the validator encounters tel-national on a type="tel" input, it flags the mismatch because the autofill field name is not allowed in that context.
This might seem counterintuitive — a national telephone number value on a telephone input feels like a natural fit. However, the distinction exists because type="tel" already implies a complete telephone number, and the spec maps the broader tel autocomplete token to it. The more granular telephone tokens like tel-national, tel-country-code, tel-area-code, tel-local, tel-local-prefix, and tel-local-suffix are designed for type="text" inputs where a phone number is being broken into individual parts across multiple fields.
Getting this right matters for browser autofill behavior. When the autocomplete value and input type are properly paired according to the spec, browsers can more reliably populate the field with the correct portion of the user's stored phone number. An invalid pairing may cause autofill to silently fail or behave unpredictably across different browsers.
How to fix it
You have two options:
- Change the input type to
text— Usetype="text"if you specifically want the national portion of a phone number (without the country code). This is the right choice when you're splitting a phone number across multiple fields. - Change the autocomplete value to
tel— Useautocomplete="tel"if you want a single field for the full phone number. This pairs correctly withtype="tel".
Examples
❌ Invalid: tel-national on type="tel"
<labelfor="phone">Phone number</label>
<inputid="phone"name="phone"type="tel"autocomplete="tel-national">
This triggers the validation error because tel-national is not allowed on a type="tel" input.
✅ Fix option 1: Change input type to text
<labelfor="phone">Phone number (without country code)</label>
<inputid="phone"name="phone"type="text"autocomplete="tel-national">
Using type="text" satisfies the spec's requirement for the tel-national autofill token. This is ideal when collecting just the national portion of a number.
✅ Fix option 2: Change autocomplete to tel
<labelfor="phone">Phone number</label>
<inputid="phone"name="phone"type="tel"autocomplete="tel">
Using autocomplete="tel" is the correct pairing for type="tel" and tells the browser to autofill the complete phone number.
✅ Splitting a phone number across multiple fields
When you need separate fields for different parts of a phone number, use type="text" with the granular autocomplete tokens:
<fieldset>
<legend>Phone number</legend>
<labelfor="country-code">Country code</label>
<inputid="country-code"name="country-code"type="text"autocomplete="tel-country-code">
<labelfor="national">National number</label>
<inputid="national"name="national"type="text"autocomplete="tel-national">
</fieldset>
The xml:lang attribute is a holdover from XHTML, where it was the standard way to declare the language of an element. In HTML5 (the text/html serialization), the lang attribute is the proper way to specify language. The HTML specification allows xml:lang for compatibility purposes, but only if it is accompanied by a lang attribute with an identical value. If xml:lang appears alone, or if its value doesn't match the lang attribute, the document is non-conforming.
This matters for several reasons. Screen readers and other assistive technologies rely on the lang attribute—not xml:lang—to determine pronunciation and language-specific behavior. Search engines also use lang for content indexing and language detection. Having xml:lang without lang means the language declaration may be ignored entirely, degrading both accessibility and SEO.
In modern HTML5 documents, there is rarely a reason to include xml:lang at all. The lang attribute alone covers all use cases. The only scenario where you might need both is if your document must be compatible with both HTML and XHTML parsers (polyglot markup), in which case the two attributes must carry the same value.
How to Fix
You have two options:
- Remove
xml:langand use onlylang(recommended for HTML5 documents). - Add a
langattribute that matches the existingxml:langvalue (for polyglot documents).
If you do keep both attributes, make sure the values are exactly the same—including case and subtags. For example, lang="en-US" must be paired with xml:lang="en-US", not xml:lang="en".
Examples
Incorrect: xml:lang without lang
<htmlxml:lang="en">
<head>
<title>My Page</title>
</head>
<body>
<pxml:lang="fr">Bonjour le monde</p>
</body>
</html>
This triggers the validation error because both the <html> and <p> elements have xml:lang but no lang attribute.
Incorrect: Mismatched values
<htmllang="en"xml:lang="en-US">
<head>
<title>My Page</title>
</head>
<body>
<p>Hello world</p>
</body>
</html>
Even though both attributes are present, the values "en" and "en-US" don't match, which is also invalid.
Correct: Using only lang (recommended)
<!DOCTYPE html>
<htmllang="en">
<head>
<title>My Page</title>
</head>
<body>
<plang="fr">Bonjour le monde</p>
</body>
</html>
This is the cleanest approach for HTML5 documents. The lang attribute is all you need.
Correct: Both attributes with matching values (polyglot)
<!DOCTYPE html>
<htmllang="en"xml:lang="en">
<head>
<title>My Page</title>
</head>
<body>
<plang="fr"xml:lang="fr">Bonjour le monde</p>
</body>
</html>
If you must keep xml:lang, every element that has it must also have lang with the exact same value.
The alt attribute is one of the most important accessibility features in HTML. When a screen reader encounters an <img> element, it reads the alt text aloud so that visually impaired users understand the image's content and purpose. Without it, screen readers may fall back to reading the file name (e.g., "DSC underscore 0042 dot jpeg"), which is meaningless and confusing. Search engines also use alt text to understand and index image content, so including it benefits SEO as well.
The HTML specification requires the alt attribute on all <img> elements, with only narrow exceptions (such as when the image's role is explicitly overridden via certain ARIA attributes, or when the image is inside a <figure> with a <figcaption> that fully describes it—though even then, including alt is strongly recommended).
How to choose the right alt text
The value of the alt attribute depends on the image's purpose:
- Informative images — Describe the content concisely. For example, a photo of a product should describe the product.
- Functional images — Describe the action or destination, not the image itself. For example, a search icon used as a button should have
alt="Search", notalt="Magnifying glass". - Decorative images — Use an empty
altattribute (alt=""). This tells screen readers to skip the image entirely. Do not omit the attribute—use an empty string. - Complex images (charts, diagrams) — Provide a brief summary in
altand a longer description elsewhere on the page or via a link.
Examples
❌ Missing alt attribute
This triggers the W3C validation error:
<imgsrc="photo.jpg">
A screen reader has no useful information to convey, and the validator flags this as an error.
✅ Informative image with descriptive alt
<imgsrc="photo.jpg"alt="Person holding an orange tabby cat in a sunlit garden">
The alt text describes what the image shows, giving screen reader users meaningful context.
✅ Decorative image with empty alt
<imgsrc="decorative-border.png"alt="">
When an image is purely decorative and adds no information, an empty alt attribute tells assistive technology to ignore it. This is valid and preferred over omitting the attribute.
✅ Functional image inside a link
<ahref="/home">
<imgsrc="logo.svg"alt="Acme Corp — Go to homepage">
</a>
Because the image is the only content inside the link, the alt text must describe the link's purpose.
✅ Image inside a <figure> with <figcaption>
<figure>
<imgsrc="chart.png"alt="Bar chart showing quarterly revenue growth from Q1 to Q4 2024">
<figcaption>Quarterly revenue growth for fiscal year 2024.</figcaption>
</figure>
Even when a <figcaption> is present, including a descriptive alt attribute is best practice. The alt should describe the image itself, while the <figcaption> provides additional context visible to all users.
Common mistakes to avoid
- Don't start with "Image of" or "Picture of" — Screen readers already announce the element as an image. Write
alt="Golden retriever playing fetch", notalt="Image of a golden retriever playing fetch". - Don't use the file name —
alt="IMG_4392.jpg"is not helpful. - Don't duplicate surrounding text — If the text next to the image already describes it, use
alt=""to avoid redundancy. - Don't omit
altthinking it's optional — A missingaltattribute andalt=""are fundamentally different. Missing means the screen reader may announce the file path; empty means the screen reader intentionally skips the image.
The autocomplete="new-password" value can only be used on <input> elements whose type accepts password input, specifically type="password".
The autocomplete attribute helps browsers autofill form fields. However, certain autofill tokens are restricted to specific input types. The new-password token tells the browser to suggest a new, generated password — which only makes sense on a password field. If you use it on a type="text", type="email", or other non-password input, the validator will flag it as invalid.
The same restriction applies to current-password. Both tokens are exclusively valid on <input type="password">.
Invalid Example
<labelfor="pass">Create a password</label>
<inputtype="text"id="pass"autocomplete="new-password">
Valid Example
<labelfor="pass">Create a password</label>
<inputtype="password"id="pass"autocomplete="new-password">
If your field is not meant to collect a password, use a different autocomplete value appropriate for the input type, such as username, email, or off.
The HTML specification defines specific rules about which autocomplete values can be used on which form elements. The street-address token is categorized as a "multiline" autofill field because street addresses often span multiple lines (e.g., "123 Main St\nApt 4B"). Since <input> elements only accept single-line text, the spec prohibits using street-address with them. The <textarea> element, on the other hand, naturally supports multiline content, making it the appropriate host for this token.
This matters for several reasons. First, browsers use autocomplete values to offer autofill suggestions. When the element type doesn't match the expected data format, browsers may not autofill correctly or may ignore the hint entirely. Second, standards compliance ensures consistent behavior across different browsers and assistive technologies. Third, using the correct pairing improves the user experience — users expect their full street address to appear in a field that can actually display it properly.
You have two approaches to fix this:
Use a
<textarea>— If you want the full street address in a single field, switch from<input>to<textarea>. This is the most semantically correct choice when you expect multiline address data.Use line-specific tokens on
<input>elements — If your form design uses separate single-line fields for each part of the address, useaddress-line1,address-line2, andaddress-line3instead. These tokens are explicitly allowed on<input>elements.
Examples
❌ Invalid: street-address on an <input>
<labelfor="address">Street Address</label>
<inputtype="text"id="address"name="address"autocomplete="street-address">
This triggers the validation error because street-address requires a multiline control.
✅ Fix: Use a <textarea> with street-address
<labelfor="address">Street Address</label>
<textareaid="address"name="address"autocomplete="street-address"></textarea>
The <textarea> supports multiline text, so street-address is valid here.
✅ Fix: Use line-specific tokens on <input> elements
<labelfor="address1">Address Line 1</label>
<inputtype="text"id="address1"name="address1"autocomplete="address-line1">
<labelfor="address2">Address Line 2</label>
<inputtype="text"id="address2"name="address2"autocomplete="address-line2">
The address-line1, address-line2, and address-line3 tokens are single-line autofill fields and are perfectly valid on <input> elements. This approach is common in forms that break the address into separate fields for apartment numbers, building names, or other details.
Summary of allowed pairings
| Token | <input> | <textarea> |
|---|---|---|
street-address | ❌ Not allowed | ✅ Allowed |
address-line1 | ✅ Allowed | ✅ Allowed |
address-line2 | ✅ Allowed | ✅ Allowed |
address-line3 | ✅ Allowed | ✅ Allowed |
Choose the approach that best fits your form layout. If you prefer a single address field, use <textarea> with street-address. If you prefer structured, separate fields, use <input> elements with the appropriate address-line tokens.
In URLs, percent-encoding is used to represent special or reserved characters. The format is a % sign followed by exactly two hexadecimal digits (0–9, A–F), such as %20 for a space or %3F for a question mark. When the browser encounters a % in a URL, it expects the next two characters to be valid hex digits. If they aren't — for example, % followed by a letter like G, a non-hex character, or nothing at all — the URL is considered malformed.
This issue specifically targets the srcset attribute on <source> elements (commonly used inside <picture> elements), where one or more image candidate URLs contain an invalid percent sequence. The most common causes are:
- A literal
%sign in a query parameter — e.g.,?quality=80%where%is meant literally but isn't encoded. - Truncated percent-encoding — e.g.,
%2instead of%2F, possibly from a copy-paste error or a broken URL-encoding function. - Double-encoding gone wrong — a URL that was partially encoded, leaving some
%characters in an ambiguous state.
This matters because browsers may handle malformed URLs inconsistently. Some browsers might try to recover gracefully, while others may fail to load the image entirely. Invalid URLs also break standards compliance, can cause issues with CDNs and caching layers, and make your markup unreliable across different environments.
How to fix it
- Find the offending
%character in yoursrcsetURL. - If the
%is meant literally (e.g., as part of a percentage value like80%), encode it as%25. - If the
%is part of an incomplete percent-encoding (e.g.,%2instead of%2F), correct it to the full three-character sequence. - Review your URL-generation logic — if URLs are built dynamically by a CMS, template engine, or server-side code, ensure proper encoding is applied before output.
Examples
Invalid: literal % in srcset URL
The % in quality=80% is not followed by two hex digits, so it's treated as a broken percent-encoding sequence.
<picture>
<source
srcset="https://example.com/photo.webp?quality=80%"
type="image/webp">
<imgsrc="https://example.com/photo.jpg"alt="A scenic landscape">
</picture>
Valid: % encoded as %25
Replacing the bare % with %25 produces a valid URL.
<picture>
<source
srcset="https://example.com/photo.webp?quality=80%25"
type="image/webp">
<imgsrc="https://example.com/photo.jpg"alt="A scenic landscape">
</picture>
Invalid: truncated percent-encoding in a file path
Here, %2 is incomplete — it should be %2F (which decodes to /) or some other valid sequence.
<picture>
<source
srcset="https://example.com/images%2photo.webp 1x"
type="image/webp">
<imgsrc="https://example.com/photo.jpg"alt="Product photo">
</picture>
Valid: corrected percent-encoding
The sequence is now %2F, a properly formed encoding.
<picture>
<source
srcset="https://example.com/images%2Fphoto.webp 1x"
type="image/webp">
<imgsrc="https://example.com/photo.jpg"alt="Product photo">
</picture>
Invalid: unencoded % in srcset with multiple candidates
Every URL in the srcset list must be valid. Here, both candidates contain a bare %.
<picture>
<source
srcset="https://example.com/img.webp?w=480&q=75% 480w, https://example.com/img.webp?w=800&q=75% 800w"
https://example.com/img.webp?w=800&q=75% 800w
type="image/webp">
<imgsrc="https://example.com/img.jpg"alt="Blog header image">
</picture>
Valid: all candidates properly encoded
<picture>
<source
srcset="https://example.com/img.webp?w=480&q=75%25 480w, https://example.com/img.webp?w=800&q=75%25 800w"
https://example.com/img.webp?w=800&q=75%25 800w
type="image/webp">
<imgsrc="https://example.com/img.jpg"alt="Blog header image">
</picture>
Note that in the corrected example, & is also encoded as & within the HTML attribute, which is required for valid markup when using ampersands in attribute values.
The lang attribute on the <html> element tells browsers, screen readers, and search engines what language the page content is written in. Its value must follow the BCP 47 standard, which uses ISO 639 language codes as the primary language subtag. When the validator reports that a language subtag "is not a valid ISO language part of a language tag," it means the code you provided doesn't match any recognized language in the ISO 639 registry.
Common Causes
Typos in the language code
A simple misspelling like emg instead of en, or fre instead of fr, will trigger this error.
Using a country code instead of a language code
Country codes (ISO 3166) and language codes (ISO 639) are different standards. For example, uk is the country code for the United Kingdom, but it's also the valid language code for Ukrainian. Using gb (Great Britain) as a language would be invalid. Similarly, us is not a language code — you need en for English or en-US for American English specifically.
Using made-up or deprecated codes
Codes like xx, en-UK, or other non-standard values will fail validation. Note that while en-US and en-GB are valid (language-region format), en-UK is not because UK is not the correct ISO 3166-1 region subtag for the United Kingdom — GB is.
Why This Matters
- Accessibility: Screen readers rely on the
langattribute to select the correct pronunciation rules and voice profile. An invalid language code can cause assistive technology to mispronounce content or fall back to a default language. - SEO: Search engines use the
langattribute as a signal for serving the right content to users in the appropriate language and region. - Browser behavior: Browsers use the language tag for spell-checking, hyphenation, font selection, and other language-sensitive rendering decisions.
How to Fix It
- Identify the language your page is written in.
- Look up the correct ISO 639-1 two-letter code (preferred) or ISO 639-2 three-letter code for that language.
- If you need to specify a regional variant, append a hyphen and the ISO 3166-1 region code (e.g.,
pt-BRfor Brazilian Portuguese). - Replace the invalid value in the
langattribute.
Some commonly used valid language codes:
| Language | Code |
|---|---|
| English | en |
| English (US) | en-US |
| English (UK) | en-GB |
| Spanish | es |
| French | fr |
| German | de |
| Portuguese (Brazil) | pt-BR |
| Chinese (Simplified) | zh-Hans |
| Japanese | ja |
| Arabic | ar |
Examples
❌ Invalid: Typo in language code
<htmllang="emg">
❌ Invalid: Country code used instead of language code
<htmllang="us">
❌ Invalid: Incorrect region subtag
<htmllang="en-UK">
❌ Invalid: Made-up language code
<htmllang="english">
✅ Valid: Correct two-letter language code
<htmllang="en">
✅ Valid: Language with region subtag
<htmllang="en-GB">
✅ Valid: Full document with proper lang attribute
<!DOCTYPE html>
<htmllang="fr">
<head>
<metacharset="utf-8">
<title>Ma page</title>
</head>
<body>
<p>Bonjour le monde !</p>
</body>
</html>
You can verify your language tag using the IANA Language Subtag Registry or the BCP 47 Language Subtag Lookup tool to ensure your code is valid before updating your markup.
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