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.
Direct text nodes inside select are not permitted content. Browsers typically ignore or mangle that text, leading to inconsistent rendering and confusing experiences for screen reader users. It also breaks conformance, which can impact maintainability and automated tooling. The right approach is to keep instructional text in a corresponding label, or provide a non-selectable prompt using a disabled, selected option. Group labels must be provided with optgroup elements, not free text.
To fix it, remove any raw text inside the select. If you need a prompt, add a first option with value="" and disabled selected hidden for a placeholder-like experience, or rely on a visible label. Ensure all selectable items are wrapped in option, and any grouping uses optgroup with its label attribute. Always associate the select with a label via for/id for accessibility.
Examples
Triggers the error (text node inside select)
<select>
Please select an option:
<optionvalue="1">Option 1</option>
<optionvalue="2">Option 2</option>
</select>
Correct: move instructions to a label
<labelfor="flavor">Please select a flavor:</label>
<selectid="flavor"name="flavor">
<optionvalue="vanilla">Vanilla</option>
<optionvalue="chocolate">Chocolate</option>
</select>
Correct: provide a non-selectable prompt inside select
<labelfor="country">Country</label>
<selectid="country"name="country"required>
<optionvalue=""disabledselectedhidden>Select a country</option>
<optionvalue="us">United States</option>
<optionvalue="ca">Canada</option>
</select>
Correct: use optgroup for grouping, not free text
<labelfor="city">City</label>
<selectid="city"name="city">
<optgrouplabel="USA">
<optionvalue="nyc">New York</option>
<optionvalue="la">Los Angeles</option>
</optgroup>
<optgrouplabel="Canada">
<optionvalue="toronto">Toronto</option>
<optionvalue="vancouver">Vancouver</option>
</optgroup>
</select>
Correct: full document (for context)
<!doctype html>
<htmllang="en">
<head>
<metacharset="utf-8">
<title>Select without stray text</title>
</head>
<body>
<form>
<labelfor="size">Choose a size:</label>
<selectid="size"name="size">
<optionvalue=""disabledselectedhidden>Select a size</option>
<optionvalue="s">Small</option>
<optionvalue="m">Medium</option>
<optionvalue="l">Large</option>
</select>
</form>
</body>
</html>
Tips:
- Put instructions in a
labelor surrounding text, not insideselect. - Every choice must be an
option; useoptgroupwithlabelto name groups. - For placeholders, prefer a disabled, selected first
option; avoid raw text nodes.
According to the HTML specification, the content model for <ul> is strictly limited to zero or more <li> elements. Any text node placed directly inside the <ul> violates this rule, even if it seems harmless or invisible. Browsers may still render the page, but the resulting DOM structure is technically invalid and can lead to unpredictable behavior across different browsers and assistive technologies.
This matters for accessibility because screen readers rely on proper list structure to announce the number of items and allow users to navigate between them. Stray text nodes inside a <ul> can confuse these tools, causing list items to be miscounted or the text to be read in an unexpected context.
There are several common scenarios that trigger this error:
Loose text used as a list title. Developers sometimes place a heading or label directly inside the <ul> to describe the list. This text must be moved outside the list element.
Stray or other entities between list items. This often happens in templating systems or when code is concatenated, where characters or other text nodes end up between <li> elements. These should be removed entirely, since spacing between list items should be controlled with CSS.
Accidentally placing inline content without wrapping it in <li>. Sometimes content that should be a list item is simply missing its <li> wrapper.
Examples
❌ Text used as a list title inside <ul>
<ul>
Fruits
<li>Apple</li>
<li>Orange</li>
<li>Banana</li>
</ul>
The word "Fruits" is a text node directly inside the <ul>, which is not allowed.
✅ Move the title outside the list
<h3>Fruits</h3>
<ul>
<li>Apple</li>
<li>Orange</li>
<li>Banana</li>
</ul>
Using a heading before the list is semantically clear. You can also use a <p> or <span> if a heading isn't appropriate.
❌ entities between list items
<ul>
<li>First item</li>
<li>Second item</li>
<li>Third item</li>
</ul>
Each is a text node sitting directly inside the <ul>, triggering the error.
✅ Remove the entities and use CSS for spacing
<ul>
<li>First item</li>
<li>Second item</li>
<li>Third item</li>
</ul>
ulli{
margin-bottom:0.5em;
}
Any visual spacing between list items should be handled with CSS margin or padding, not with HTML entities.
❌ Unwrapped content that should be a list item
<ul>
<li>Milk</li>
Eggs
<li>Bread</li>
</ul>
✅ Wrap the content in an <li> element
<ul>
<li>Milk</li>
<li>Eggs</li>
<li>Bread</li>
</ul>
The same rules apply to <ol> (ordered lists) and <menu> elements — their direct children must be <li> elements, and text nodes are not permitted. If your list is generated dynamically by a templating engine or JavaScript, check the output carefully for stray whitespace or text that may have been injected between list items.
Unicode allows some characters to be represented in multiple ways. For example, the accented letter "é" can be stored as a single precomposed character (U+00E9) or as two separate code points: the base letter "e" (U+0065) followed by a combining acute accent (U+0301). While these look identical when rendered, they are fundamentally different at the byte level. Unicode Normalization Form C (NFC) is the canonical form that prefers the single precomposed representation whenever one exists.
The HTML specification and the W3C Character Model for the World Wide Web require that all text in HTML documents be in NFC. This matters for several reasons:
- String matching and search: Non-NFC text can cause failures when browsers or scripts try to match strings, compare attribute values, or process CSS selectors. Two visually identical strings in different normalization forms won't match with simple byte comparison.
- Accessibility: Screen readers and assistive technologies may behave inconsistently when encountering decomposed character sequences.
- Interoperability: Different browsers, search engines, and tools may handle non-NFC text differently, leading to unpredictable behavior.
- Fragment identifiers and IDs: If an
idattribute contains non-NFC characters, fragment links (#id) may fail to work correctly.
This issue most commonly appears when text is copied from word processors, PDFs, or other applications that use decomposed Unicode forms (NFD), or when content is generated by software that doesn't normalize its output.
How to Fix It
- Identify the affected text: The validator will point to the specific line containing non-NFC characters. The characters will often look normal visually, so you'll need to inspect them at the code-point level.
- Convert to NFC: Use a text editor or command-line tool that supports Unicode normalization. Many programming languages provide built-in normalization functions.
- Prevent future issues: Configure your text editor or build pipeline to save files in NFC. When accepting user input, normalize it server-side before storing or embedding in HTML.
In Python, you can normalize a string:
importunicodedata
normalized=unicodedata.normalize('NFC', original_string)
In JavaScript (Node.js or browser):
constnormalized=originalString.normalize('NFC');
On the command line (using uconv from ICU):
uconv-xNFCinput.html>output.html
Examples
Incorrect (decomposed form — NFD)
In this example, the letter "é" is represented as two code points (e + combining acute accent), which triggers the validation warning. The source may look identical to the correct version, but the underlying bytes differ:
<!-- "é" here is stored as U+0065 U+0301 (decomposed) -->
<p>Résumé available upon request.</p>
Correct (precomposed form — NFC)
Here, the same text uses the single precomposed character é (U+00E9):
<!-- "é" here is stored as U+00E9 (precomposed) -->
<p>Résumé available upon request.</p>
Incorrect in attributes
Non-NFC text in attribute values also triggers this issue:
<!-- The id contains a decomposed character -->
<h2id="resumé">Résumé</h2>
Correct in attributes
<!-- The id uses the precomposed NFC character -->
<h2id="resumé">Résumé</h2>
While these examples look the same in rendered text, the difference is in how the characters are encoded. To verify your text is in NFC, you can paste it into a Unicode inspector tool or use the normalization functions mentioned above. For further reading, the W3C provides an excellent guide on Normalization in HTML and CSS.
When you write a <table> without explicitly using <thead> and <tbody>, the HTML parser automatically wraps your <tr> elements in an implicit <tbody>. If any of those rows contain <th> elements intended as column headers, the validator flags them because <th> cells in <tbody> are unexpected — the parser sees header cells appearing in what should be the data body of the table.
While <th> elements are technically valid inside <tbody> (for example, as row headers), this warning usually indicates a structural problem: your column headers aren't properly separated from your data rows. Properly structuring your table with <thead> and <tbody> matters for several reasons:
- Accessibility: Screen readers use table structure to help users navigate. A
<thead>section clearly identifies column headers, making it easier for assistive technology to announce what each data cell represents. - Styling and behavior: CSS selectors like
thead thandtbody tdlet you target headers and data cells independently. Browsers can also use<thead>and<tbody>to enable scrollable table bodies while keeping headers fixed. - Standards compliance: Explicitly defining table sections removes ambiguity and ensures consistent parsing across all browsers.
To fix this issue, wrap the row containing your <th> column headers in a <thead> element, and wrap your data rows in a <tbody> element.
Examples
❌ Incorrect: <th> in implicit table body
Here, the parser wraps all rows in an implicit <tbody>, so the <th> elements end up inside the table body:
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Liza</td>
<td>49</td>
</tr>
<tr>
<td>Joe</td>
<td>47</td>
</tr>
</table>
✅ Correct: <th> in explicit <thead>
Wrapping the header row in <thead> and data rows in <tbody> resolves the issue:
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr>
<td>Liza</td>
<td>49</td>
</tr>
<tr>
<td>Joe</td>
<td>47</td>
</tr>
</tbody>
</table>
✅ Correct: <th> as row headers inside <tbody>
If you intentionally use <th> elements inside <tbody> as row headers, add the scope attribute to clarify their purpose. This is valid and won't trigger the warning when the table also has a proper <thead>:
<table>
<thead>
<tr>
<thscope="col">Name</th>
<thscope="col">Age</th>
</tr>
</thead>
<tbody>
<tr>
<thscope="row">Liza</th>
<td>49</td>
</tr>
<tr>
<thscope="row">Joe</th>
<td>47</td>
</tr>
</tbody>
</table>
The scope="row" attribute tells assistive technology that these <th> cells are headers for their respective rows, while scope="col" identifies column headers. This combination provides the best accessibility for table data.
In earlier versions of HTML, there were two separate elements for shortened forms of words and phrases: <abbr> for abbreviations (like "Dr." or "etc.") and <acronym> for acronyms (like "NASA" or "HTML"). The HTML5 specification eliminated this distinction because acronyms are simply a type of abbreviation. The <abbr> element now covers all cases.
Using the obsolete <acronym> element causes W3C validation errors and has several practical drawbacks:
- Standards compliance: The element is not part of the current HTML specification. Validators will flag it as an error, and future browsers are not guaranteed to support it.
- Accessibility: Assistive technologies are designed and tested against current standards. While many screen readers still handle
<acronym>, relying on an obsolete element risks inconsistent behavior. The<abbr>element has well-defined, standardized accessibility semantics. - Consistency: Using
<abbr>for all abbreviations and acronyms simplifies your markup and makes it easier for developers to maintain.
The fix is straightforward: replace every <acronym> tag with <abbr>. The title attribute works the same way on both elements — it provides the expanded form of the abbreviation or acronym that browsers typically display as a tooltip on hover.
Examples
❌ Obsolete: using <acronym>
<p>The <acronymtitle="World Wide Web">WWW</acronym> was invented by Tim Berners-Lee.</p>
<p>This page is written in <acronymtitle="HyperText Markup Language">HTML</acronym>.</p>
✅ Fixed: using <abbr>
<p>The <abbrtitle="World Wide Web">WWW</abbr> was invented by Tim Berners-Lee.</p>
<p>This page is written in <abbrtitle="HyperText Markup Language">HTML</abbr>.</p>
Using <abbr> for both abbreviations and acronyms
Since <abbr> now handles all shortened forms, you can use it consistently throughout your markup:
<p>
Contact <abbrtitle="Doctor">Dr.</abbr> Smith at
<abbrtitle="National Aeronautics and Space Administration">NASA</abbr>
for more information about the <abbrtitle="International Space Station">ISS</abbr>.
</p>
Styling <abbr> with CSS
Some browsers apply a default dotted underline to <abbr> elements with a title attribute. You can customize this with CSS:
<style>
abbr[title]{
text-decoration: underline dotted;
cursor: help;
}
</style>
<p>Files are transferred using <abbrtitle="File Transfer Protocol">FTP</abbr>.</p>
If you're migrating a large codebase, a simple find-and-replace of <acronym with <abbr and </acronym> with </abbr> will handle the conversion. No other attributes or content changes are needed — the two elements accept the same attributes and content model.
In earlier versions of HTML, the align attribute was used directly on <td> (and other table elements like <tr> and <th>) to control horizontal alignment of cell content. Values like left, center, right, and justify were common. HTML5 made this attribute obsolete in favor of CSS, which provides a cleaner separation of content and presentation.
While most browsers still honor the align attribute for backward compatibility, relying on it is discouraged. It violates modern web standards, mixes presentational concerns into your markup, and makes styling harder to maintain. CSS offers far more flexibility — you can target cells by class, use responsive styles, or change alignment through media queries without touching your HTML.
How to fix it
- Remove the
alignattribute from the<td>element. - Apply the CSS
text-alignproperty instead, either via an inlinestyleattribute, a<style>block, or an external stylesheet.
The CSS text-align property accepts the same values the old attribute did: left, center, right, and justify.
For vertical alignment, the obsolete valign attribute should similarly be replaced with the CSS vertical-align property.
Examples
❌ Obsolete: using the align attribute
<table>
<tr>
<tdalign="center">Centered content</td>
<tdalign="right">Right-aligned content</td>
</tr>
</table>
✅ Fixed: using inline CSS
<table>
<tr>
<tdstyle="text-align: center;">Centered content</td>
<tdstyle="text-align: right;">Right-aligned content</td>
</tr>
</table>
✅ Fixed: using a stylesheet (recommended)
Using classes keeps your HTML clean and makes it easy to update styles across your entire site.
<style>
.text-center{
text-align: center;
}
.text-right{
text-align: right;
}
</style>
<table>
<tr>
<tdclass="text-center">Centered content</td>
<tdclass="text-right">Right-aligned content</td>
</tr>
</table>
✅ Fixed: applying alignment to an entire column
If every cell in a column needs the same alignment, you can target cells by position instead of adding a class to each one.
<style>
td:nth-child(2){
text-align: right;
}
</style>
<table>
<tr>
<td>Item</td>
<td>$9.99</td>
</tr>
<tr>
<td>Another item</td>
<td>$14.50</td>
</tr>
</table>
This same approach applies to <th> elements and the <tr> element, which also had an obsolete align attribute in older HTML. In all cases, replace the attribute with the CSS text-align property.
The aria-checked attribute is not allowed on a label element when that label is associated with a form control like a checkbox or radio button.
When a label is associated with a labelable element (via the for attribute or by nesting), the label acts as an accessible name provider — it doesn't represent the control's state itself. The aria-checked attribute is meant for elements that act as a checkbox or radio button, such as elements with role="checkbox" or role="switch", not for labels that merely describe one.
Adding aria-checked to a label creates conflicting semantics. Assistive technologies already read the checked state from the associated input element, so duplicating or overriding that state on the label causes confusion.
If you need a custom toggle or checkbox, apply aria-checked to the element that has the interactive role, not to the label.
Example with the issue
<labelfor="notifications"aria-checked="true">Enable notifications</label>
<inputtype="checkbox"id="notifications"checked>
Fixed example using a native checkbox
<labelfor="notifications">Enable notifications</label>
<inputtype="checkbox"id="notifications"checked>
The native <input type="checkbox"> already communicates its checked state to assistive technologies — no aria-checked is needed anywhere.
Fixed example using a custom toggle
If you're building a custom control without a native checkbox, apply aria-checked to the element with the appropriate role:
<spanid="toggle-label">Enable notifications</span>
<spanrole="switch"tabindex="0"aria-checked="true"aria-labelledby="toggle-label"></span>
Here, aria-checked is correctly placed on the element with role="switch", which is the interactive control. The label is a separate <span> referenced via aria-labelledby, keeping roles and states cleanly separated.
The aria-controls attribute establishes a programmatic relationship between a controlling element (like a button, tab, or scrollbar) and the element it controls (like a panel, region, or content area). Assistive technologies such as screen readers use this relationship to help users navigate between related elements — for example, announcing that a button controls a specific panel and allowing the user to jump to it.
When the id referenced in aria-controls doesn't exist in the document, the relationship is broken. Screen readers may attempt to locate the target element and fail silently, or they may announce a control relationship that leads nowhere. This degrades the experience for users who rely on assistive technology and violates the WAI-ARIA specification, which requires that the value of aria-controls be a valid ID reference list pointing to elements in the same document.
Common causes of this error include:
- Typos in the
idor thearia-controlsvalue. - Dynamically generated content where the controlled element hasn't been rendered yet or has been removed from the DOM.
- Copy-paste errors where
aria-controlswas copied from another component but the correspondingidwas not updated. - Referencing elements in iframes or shadow DOM, which are considered separate document contexts.
The aria-controls attribute accepts one or more space-separated ID references. Every listed ID must match an element in the same document.
How to Fix
- Verify the target element exists in the document and has the exact
idthataria-controlsreferences. - Check for typos — ID matching is case-sensitive, so
mainPanelandmainpanelare not the same. - If the controlled element is added dynamically, ensure it is present in the DOM before or at the same time as the controlling element, or update
aria-controlsprogrammatically when the target becomes available. - If the controlled element is genuinely absent (e.g., conditionally rendered), remove the
aria-controlsattribute until the target element exists.
Examples
Incorrect: aria-controls references a non-existent ID
<buttonaria-controls="info-panel"aria-expanded="false">
Toggle Info
</button>
<divid="infopanel">
<p>Here is some additional information.</p>
</div>
This triggers the error because aria-controls="info-panel" does not match the actual id of "infopanel" (note the missing hyphen).
Correct: aria-controls matches an existing element's ID
<buttonaria-controls="info-panel"aria-expanded="false">
Toggle Info
</button>
<divid="info-panel">
<p>Here is some additional information.</p>
</div>
Correct: Tab and tab panel relationship
<divrole="tablist">
<buttonrole="tab"aria-controls="tab1-panel"aria-selected="true">
Overview
</button>
<buttonrole="tab"aria-controls="tab2-panel"aria-selected="false">
Details
</button>
</div>
<divid="tab1-panel"role="tabpanel">
<p>Overview content goes here.</p>
</div>
<divid="tab2-panel"role="tabpanel"hidden>
<p>Details content goes here.</p>
</div>
Both aria-controls values — tab1-panel and tab2-panel — correctly correspond to elements present in the document.
Correct: Custom scrollbar controlling a region
<divrole="scrollbar"aria-controls="main-content"aria-valuenow="0"aria-valuemin="0"aria-valuemax="100"aria-orientation="vertical"></div>
<divid="main-content"role="region"aria-label="Main content">
<p>Scrollable content goes here.</p>
</div>
Correct: Controlling multiple elements
The aria-controls attribute can reference multiple IDs separated by spaces. Each ID must exist in the document.
<buttonaria-controls="section-a section-b">
Expand All Sections
</button>
<divid="section-a">
<p>Section A content.</p>
</div>
<divid="section-b">
<p>Section B content.</p>
</div>
The aria-describedby attribute is a core part of WAI-ARIA, the Web Accessibility Initiative's specification for making web content more accessible. It works by creating a relationship between an element and one or more other elements that provide additional descriptive text. Screen readers and other assistive technologies use this relationship to announce the descriptive text when a user interacts with the element.
When you set aria-describedby="some-id", the browser looks for an element with id="some-id" in the same document. If no matching element exists, the reference is broken. This means assistive technologies cannot find the description, and the attribute silently does nothing. The W3C validator flags this as an error because a dangling reference indicates a bug — either the referenced element was removed, renamed, or was never added.
This issue commonly arises due to:
- Typos in the
idvalue — thearia-describedbyvalue doesn't match the target element'sidexactly (the match is case-sensitive). - Dynamic content — the described-by element is rendered conditionally or injected by JavaScript after validation.
- Copy-paste errors — markup was copied from another page or component, but the referenced element wasn't included.
- Refactoring — an element's
idwas changed or the element was removed, but thearia-describedbyreference wasn't updated.
Multiple id values can be listed in aria-describedby, separated by spaces. Every single id in that list must resolve to an element in the document. If even one is missing, the validator will report an error for that reference.
How to fix it
- Check for typos. Compare the value in
aria-describedbyagainst theidof the target element. Remember thatidmatching is case-sensitive —helpTextandhelptextare different. - Add the missing element. If the descriptive element doesn't exist yet, create it with the matching
id. - Remove stale references. If the description is no longer needed, remove the
aria-describedbyattribute entirely rather than leaving a broken reference. - Verify all IDs in a multi-value list. If
aria-describedbycontains multiple space-separated IDs, confirm each one exists.
Examples
Broken reference (triggers the error)
In this example, aria-describedby points to password-help, but no element with that id exists in the document:
<labelfor="password">Password</label>
<inputtype="password"id="password"aria-describedby="password-help">
Fixed by adding the referenced element
Adding an element with id="password-help" resolves the issue:
<labelfor="password">Password</label>
<inputtype="password"id="password"aria-describedby="password-help">
<pid="password-help">Must be at least 8 characters with one number.</p>
Broken reference due to a typo
Here the aria-describedby value uses a different case than the element's id:
<inputtype="text"id="email"aria-describedby="emailHelp">
<smallid="emailhelp">We'll never share your email.</small>
The fix is to make the id values match exactly:
<inputtype="text"id="email"aria-describedby="email-help">
<smallid="email-help">We'll never share your email.</small>
Multiple IDs with one missing
When listing multiple descriptions, every id must be present:
<!-- "format-hint" exists but "length-hint" does not — this triggers the error -->
<inputtype="text"id="username"aria-describedby="format-hint length-hint">
<spanid="format-hint">Letters and numbers only.</span>
Fix it by adding the missing element:
<inputtype="text"id="username"aria-describedby="format-hint length-hint">
<spanid="format-hint">Letters and numbers only.</span>
<spanid="length-hint">Between 3 and 20 characters.</span>
Removing the attribute when no description is needed
If the descriptive content has been removed and is no longer relevant, simply remove the aria-describedby attribute:
<inputtype="text"id="search">
The aria-expanded attribute is redundant on a summary element when it is the direct child of a details element, because the browser already communicates the expanded/collapsed state through the native open attribute on details.
The summary element, when used as the first child of a details element, acts as the built-in toggle control. Assistive technologies already understand this relationship and automatically convey whether the disclosure widget is open or closed based on the details element's open attribute. Adding aria-expanded to summary in this context creates duplicate semantics, which can confuse screen readers by announcing the state twice.
If you're using JavaScript to toggle aria-expanded manually, you can safely remove it and rely on the native behavior instead. The details/summary pattern is one of the best examples of built-in accessibility that requires no extra ARIA attributes.
Incorrect Example
<details>
<summaryaria-expanded="false">More information</summary>
<p>Here are the additional details.</p>
</details>
Correct Example
<details>
<summary>More information</summary>
<p>Here are the additional details.</p>
</details>
If you need the section to be open by default, use the open attribute on the details element:
<detailsopen>
<summary>More information</summary>
<p>Here are the additional details.</p>
</details>
An <input type="hidden"> element is inherently invisible to all users. It is not rendered on the page, it cannot receive focus, and browsers automatically exclude it from the accessibility tree. The aria-hidden attribute is designed to hide visible content from assistive technologies like screen readers, but applying it to an element that is already fully hidden serves no purpose.
The HTML specification explicitly forbids the use of aria-hidden on hidden inputs. This restriction exists because combining the two is semantically meaningless — you cannot "hide from assistive technologies" something that is already invisible to everyone. Validators flag this as an error to encourage clean, standards-compliant markup and to help developers avoid misunderstandings about how ARIA attributes interact with native HTML semantics.
This issue commonly arises when aria-hidden="true" is applied broadly to a group of elements (for example, via a script or template) without checking whether specific children already handle their own visibility. It can also happen when developers add ARIA attributes as a precaution, not realizing that the native behavior of type="hidden" already covers accessibility concerns.
To fix this, remove the aria-hidden attribute from any <input> element whose type is hidden. No replacement is needed — the browser already handles everything correctly.
Examples
Incorrect
Adding aria-hidden to a hidden input triggers a validation error:
<formaction="/submit"method="post">
<inputtype="hidden"aria-hidden="true"name="month"value="10">
<inputtype="hidden"aria-hidden="true"name="csrf_token"value="abc123">
<buttontype="submit">Submit</button>
</form>
Correct
Remove the aria-hidden attribute entirely. The hidden inputs are already inaccessible to all users by default:
<formaction="/submit"method="post">
<inputtype="hidden"name="month"value="10">
<inputtype="hidden"name="csrf_token"value="abc123">
<buttontype="submit">Submit</button>
</form>
When aria-hidden is appropriate
The aria-hidden attribute is intended for elements that are visually present but should be hidden from assistive technologies, such as decorative icons:
<buttontype="button">
<spanaria-hidden="true">★</span>
Favorite
</button>
In this case, the decorative star is visible on screen but irrelevant to screen reader users, so aria-hidden="true" correctly prevents it from being announced. This is the proper use case — hiding visible content from the accessibility tree, not redundantly marking already-hidden elements.
The <link> element is used to define relationships between the current document and external resources—most commonly stylesheets, favicons, and preloaded assets. It is a void element (it has no closing tag) and it produces no rendered content on the page. Because <link> elements are inherently non-visible and already absent from the accessibility tree, the aria-hidden attribute serves no purpose on them.
The aria-hidden attribute is designed to control the visibility of rendered content for assistive technologies like screen readers. When set to "true" on a visible element, it tells assistive technologies to skip that element and its descendants. Applying it to a <link> element is contradictory—you're trying to hide something from assistive technologies that was never exposed to them in the first place. The HTML specification explicitly disallows this combination, and the W3C validator will flag it as an error.
This issue sometimes arises when developers apply aria-hidden broadly through templating systems, JavaScript frameworks, or build tools that inject attributes across multiple element types without distinguishing between visible content elements and metadata elements. It can also happen when copying attribute patterns from <a> elements (which share the word "link" conceptually but are entirely different elements) onto <link> elements.
How to fix it
The fix is straightforward: remove the aria-hidden attribute from the <link> element. No replacement or alternative attribute is needed because <link> elements are already invisible to assistive technologies.
If your original intent was to hide a visible element from screen readers, make sure you're applying aria-hidden to the correct element—a rendered content element such as <div>, <span>, <img>, or <a>, not a metadata element like <link>.
Examples
Incorrect: aria-hidden on a <link> element
<linkrel="stylesheet"href="styles.css"aria-hidden="true">
<linkrel="icon"href="favicon.ico"aria-hidden="true">
<linkrel="preload"href="font.woff2"as="font"type="font/woff2"aria-hidden="true">
Correct: <link> without aria-hidden
<linkrel="stylesheet"href="styles.css">
<linkrel="icon"href="favicon.ico">
<linkrel="preload"href="font.woff2"as="font"type="font/woff2">
Correct: aria-hidden used on a visible element instead
If you need to hide a decorative element from screen readers, apply aria-hidden to the rendered element itself:
<linkrel="stylesheet"href="styles.css">
<divaria-hidden="true">
<imgsrc="decorative-swirl.png"alt="">
</div>
Incorrect vs. correct in a full document
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="utf-8">
<title>My Page</title>
<!-- Incorrect: aria-hidden on link -->
<linkrel="stylesheet"href="styles.css"aria-hidden="true">
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="utf-8">
<title>My Page</title>
<!-- Correct: no aria-hidden on link -->
<linkrel="stylesheet"href="styles.css">
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
An aria-hidden attribute on a <label> element that is associated with a form control hides the label from assistive technologies while the control still expects an accessible name from it. This creates a broken association where the form field appears unlabeled to screen readers.
The aria-hidden="true" attribute removes an element and all its descendants from the accessibility tree. When a <label> is linked to an input (either by wrapping it or through the for attribute), the browser uses that label to compute the input's accessible name. Setting aria-hidden="true" on the label strips away that name, leaving the input without a label for assistive technology users.
The HTML spec explicitly forbids this combination because it guarantees an accessibility failure. A label that exists visually but is hidden from the accessibility tree defeats its own purpose.
To fix this, remove aria-hidden from the <label>. If the label text should not be visible on screen, use a CSS technique to visually hide it while keeping it accessible. If the label itself is truly decorative and another mechanism provides the accessible name (like aria-label on the input), remove the for/id association so the <label> is no longer linked to the control.
Incorrect example
<labelfor="email"aria-hidden="true">Email</label>
<inputtype="email"id="email">
Correct examples
Remove aria-hidden from the label:
<labelfor="email">Email</label>
<inputtype="email"id="email">
If the goal is to visually hide the label while keeping it accessible, use CSS instead:
<labelfor="email"class="visually-hidden">Email</label>
<inputtype="email"id="email">
<style>
.visually-hidden{
clip:rect(0000);
clip-path:inset(50%);
height:1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width:1px;
}
</style>
If the label is decorative and the input gets its accessible name from another source, break the association:
<labelaria-hidden="true">Email</label>
<inputtype="email"aria-label="Email">
An aria-label attribute on an <a> element is only valid when the link has an accessible role that supports naming — which means the <a> must have an href attribute or an explicit role that accepts a label.
When an <a> element lacks an href attribute, it has the implicit role of generic. The generic role is in the list of roles that do not support naming, so applying aria-label to it is invalid. This is because a generic element has no semantic meaning, and screen readers wouldn't know how to announce the label in a meaningful way.
The most common cause of this error is using <a> as a placeholder or JavaScript-only trigger without an href. An <a> with an href has the implicit role of link, which does support aria-label, so the error won't appear.
You have a few ways to fix this:
- Add an
hrefto make it a proper link (most common fix). - Add an explicit role that supports naming, such as
role="button", if the element acts as a button. - Use a
<button>instead if the element triggers an action rather than navigation. - Remove
aria-labelif it's not needed, and use visible text content instead.
HTML Examples
❌ Invalid: aria-label on an <a> without href
<aaria-label="Close menu"onclick="closeMenu()">✕</a>
The <a> has no href, so its implicit role is generic, which does not support naming.
✅ Fix option 1: Add an href
<ahref="/close"aria-label="Close menu">✕</a>
✅ Fix option 2: Use a <button> instead
<buttonaria-label="Close menu"onclick="closeMenu()">✕</button>
✅ Fix option 3: Add an explicit role that supports naming
<arole="button"tabindex="0"aria-label="Close menu"onclick="closeMenu()">✕</a>
Using a <button> (option 2) is generally the best choice for interactive elements that perform actions rather than navigate to a URL.
A div element without an explicit role resolves to the generic role, which does not support naming — so adding aria-label to a plain div is invalid.
The aria-label attribute provides an accessible name for an element, but not every element is allowed to have one. The ARIA specification defines certain roles as "naming prohibited," meaning assistive technologies will ignore any accessible name applied to them. The generic role is one of these, and since a div without an explicit role attribute defaults to generic, the aria-label is effectively meaningless.
To fix this, you have two main options: assign an appropriate ARIA role to the div so it becomes a nameable landmark or widget, or switch to a semantic HTML element that already carries a valid role. Common roles that support naming include region, group, navigation, alert, and many others.
If the div is truly just a generic wrapper with no semantic meaning, consider whether aria-label is even needed. Perhaps the label belongs on a child element instead, or the content is already self-describing.
HTML Examples
❌ Invalid: aria-label on a plain div
<divaria-label="User profile section">
<p>Welcome, Jane!</p>
</div>
✅ Fix: Add an appropriate role
<divrole="region"aria-label="User profile section">
<p>Welcome, Jane!</p>
</div>
✅ Fix: Use a semantic element instead
<sectionaria-label="User profile section">
<p>Welcome, Jane!</p>
</section>
The aria-label attribute cannot be used on an <i> element with its default implicit role (generic), because generic elements are not allowed to have accessible names.
The <i> element has an implicit ARIA role of generic, which is one of the roles explicitly prohibited from carrying an aria-label. This restriction exists because screen readers and other assistive technologies ignore accessible names on generic containers — so adding aria-label to a plain <i> element would silently fail to convey any meaning to users who rely on assistive technology.
This issue commonly appears when icon fonts (like Font Awesome) use <i> elements as decorative icons. If the icon is purely decorative, you should hide it from assistive technology with aria-hidden="true" and place the accessible label on a parent or sibling element instead. If the icon conveys meaning on its own, you can assign an appropriate role like role="img" so the aria-label is actually announced.
HTML Examples
❌ Invalid: aria-label on a plain <i> element
<button>
<iclass="icon-search"aria-label="Search"></i>
</button>
✅ Fix 1: Decorative icon — hide it, label the parent
<buttonaria-label="Search">
<iclass="icon-search"aria-hidden="true"></i>
</button>
✅ Fix 2: Meaningful icon — assign role="img"
<button>
<iclass="icon-search"role="img"aria-label="Search"></i>
</button>
The aria-label attribute cannot be used on a custom element like <menu-item> when it has no explicit role attribute, because it defaults to the generic role, which is in the list of roles that prohibit aria-label.
Custom elements without an explicit role are treated as having the generic role (equivalent to a <span> or <div> in terms of semantics). The WAI-ARIA specification prohibits aria-label on several roles, including generic, because naming these elements creates a confusing experience for assistive technology users — a generic container with a label doesn't convey any meaningful purpose.
To fix this, you need to assign a meaningful role to the <menu-item> element that supports accessible naming. Common choices include role="menuitem", role="link", or role="button", depending on what the element actually does. Since this appears to represent a menu item that navigates to a page, role="menuitem" is likely the most appropriate.
HTML Examples
❌ Invalid: aria-label on an element with implicit generic role
<menu-item
submenu-href="/page"
label="some label"
submenu-title="some submenu title"
aria-label="some aria label">
</menu-item>
✅ Valid: adding an explicit role that supports aria-label
<menu-item
role="menuitem"
submenu-href="/page"
label="some label"
submenu-title="some submenu title"
aria-label="some aria label">
</menu-item>
If the aria-label isn't actually needed (for example, if assistive technology already receives the label through other means in your component), another valid fix is to simply remove aria-label entirely.
A span element has an implicit ARIA role of generic, and the aria-label attribute is not allowed on elements with that role.
The span element is a generic inline container with no semantic meaning. Its default ARIA role is generic, which is one of several roles that prohibit naming via aria-label or aria-labelledby. This restriction exists because screen readers are not expected to announce names for generic containers — adding aria-label to them creates an inconsistent and confusing experience for assistive technology users.
To fix this, you have two main options:
- Assign an explicit role to the
spanthat supports naming, such asrole="img",role="group",role="status", or any other role that allowsaria-label. - Use a different element that already has a semantic role supporting
aria-label, such as abutton,a,section, ornav.
If the span is purely decorative or used for styling, consider using aria-hidden="true" instead and placing accessible text elsewhere.
HTML Examples
❌ Invalid: aria-label on a plain span
<spanaria-label="Close">✕</span>
✅ Fixed: assign an appropriate role
<spanrole="img"aria-label="Close">✕</span>
✅ Fixed: use a semantic element instead
<buttonaria-label="Close">✕</button>
✅ Fixed: hide the decorative span and provide text another way
<button>
<spanaria-hidden="true">✕</span>
<spanclass="visually-hidden">Close</span>
</button>
The <time> element does not support the aria-label attribute when it has no explicit role or when it carries certain generic roles.
The <time> element has an implicit ARIA role of time, but this role is not listed among those that allow aria-label. According to the ARIA in HTML specification, aria-label is only permitted on elements with roles that support naming from author — and the default role of <time> (as well as roles like generic, presentation, paragraph, and others listed in the error) does not qualify.
In practice, the <time> element already conveys its meaning through its visible text content and the machine-readable datetime attribute. Screen readers use the visible text to announce the date, so aria-label is typically unnecessary.
To fix this, simply remove the aria-label attribute and ensure the visible text content is descriptive enough. If you need to provide a more accessible reading of the date, you can adjust the visible text itself or wrap the element with a <span> that has an appropriate role.
Also note the original code has a missing space before datetime — the attribute must be separated from class="Tag" by a space.
HTML Examples
❌ Invalid: aria-label on <time>
<timearia-label="Apr 2."class="Tag"datetime="2026-04-02">
Apr 2.
</time>
✅ Fixed: Remove aria-label and use clear visible text
<timeclass="Tag"datetime="2026-04-02">
April 2, 2026
</time>
If you truly need aria-label, you can assign an explicit role that supports naming, such as role="text", though this is rarely necessary:
<timerole="text"aria-label="April 2nd, 2026"class="Tag"datetime="2026-04-02">
Apr 2.
</time>
The aria-label attribute is not allowed on a <label> element when that <label> contains a labelable element (such as <input>, <select>, <textarea>, or <button>).
The <label> element already provides an accessible name for its associated form control through its text content. When a <label> wraps a labelable element, adding aria-label to the <label> creates a conflict: the <label> has one accessible name (from aria-label) while the form control inside it derives its accessible name from the <label>'s text content. Assistive technologies may handle this inconsistency unpredictably.
The HTML spec restricts aria-label on <label> elements that are ancestors of labelable elements. A "labelable element" is any element that can be associated with a <label>, including <input> (except type="hidden"), <select>, <textarea>, <button>, <meter>, <output>, and <progress>.
If the <label> needs visible text, just use the text content of the <label> directly. If you need to provide an accessible name that differs from the visible text, place aria-label on the form control itself instead of on the <label>.
Examples
Invalid: aria-label on a label that wraps an input
<labelaria-label="Enter your email address">
<inputtype="email"name="email">
</label>
Fixed: move aria-label to the input
If the visible label text is sufficient, remove aria-label entirely:
<label>
<inputtype="email"name="email">
</label>
If you need a more descriptive accessible name for the input, place aria-label on the input:
<label>
<inputtype="email"name="email"aria-label="Enter your email address">
</label>
The aria-label attribute is not allowed on a label element that is associated with a form control through the for attribute.
A <label> element already provides an accessible name for the form control it's associated with. Adding aria-label to the <label> itself creates a conflict: the aria-label would attempt to override the label's own accessible name, but the label's visible text is what gets passed to the associated form control. This redundancy is not only unnecessary but explicitly prohibited by the HTML specification.
The <label> element's purpose is to be the accessible label for another element. If you want the form control to have an accessible name, simply put that text inside the <label> element as visible content. If you need to provide a different accessible name directly to the form control, place the aria-label on the input element instead.
Incorrect Example
<labelfor="input_email"id="label_input_email"aria-label="Email">
</label>
<inputtype="email"id="input_email">
Correct Example
The simplest fix is to remove the aria-label from the <label>, since the label's text content already serves as the accessible name for the input:
<labelfor="input_email"id="label_input_email">
</label>
<inputtype="email"id="input_email">
If you need the accessible name to differ from the visible label text, place aria-label on the input instead:
<labelfor="input_email"id="label_input_email">
</label>
<inputtype="email"id="input_email"aria-label="Your email address">
A div element without an explicit role (or with role="generic") cannot have the aria-labelledby attribute because generic containers have no semantic meaning that benefits from a label.
The div element maps to the generic ARIA role by default. Generic elements are purely structural — they don't represent anything meaningful to assistive technologies. Labeling something that has no semantic purpose creates a confusing experience for screen reader users, since the label points to an element that doesn't convey a clear role.
The aria-labelledby attribute is designed for interactive or landmark elements — things like dialog, region, navigation, form, or group — where a label helps users understand the purpose of that section.
To fix this, you have two options: assign a meaningful ARIA role to the div, or use a more semantic HTML element that naturally supports labeling.
HTML Examples
❌ Invalid: aria-labelledby on a plain div
<h2id="section-title">User Settings</h2>
<divaria-labelledby="section-title">
<p>Manage your account preferences here.</p>
</div>
✅ Fixed: Add a meaningful role to the div
<h2id="section-title">User Settings</h2>
<divrole="region"aria-labelledby="section-title">
<p>Manage your account preferences here.</p>
</div>
✅ Fixed: Use a semantic element instead
<h2id="section-title">User Settings</h2>
<sectionaria-labelledby="section-title">
<p>Manage your account preferences here.</p>
</section>
Using a section element or adding role="region" tells assistive technologies that this is a distinct, meaningful area of the page — making the label useful and the markup valid.
A span element has an implicit ARIA role of generic, and the aria-labelledby attribute is not allowed on elements with that role.
The span element is a generic inline container with no semantic meaning. Its default ARIA role is generic, and the ARIA specification explicitly prohibits naming generic elements with aria-labelledby (or aria-label). This restriction exists because accessible names on generic containers create confusing experiences for assistive technology users — screen readers wouldn't know what kind of thing is being labeled.
To fix this, you have two main options:
- Add a meaningful
roleto thespanthat supportsaria-labelledby, such asrole="group",role="region", or any other role that accepts a label. - Use a more semantic element that already has an appropriate role, like a
section,nav, ordivwith an explicit role.
If the span doesn't truly need a label, simply remove the aria-labelledby attribute.
HTML Examples
❌ Invalid: aria-labelledby on a plain span
<spanid="label">Settings</span>
<spanaria-labelledby="label">
<inputtype="checkbox"id="opt1">
<labelfor="opt1">Enable notifications</label>
</span>
✅ Fix: Add an appropriate role
<spanid="label">Settings</span>
<spanrole="group"aria-labelledby="label">
<inputtype="checkbox"id="opt1">
<labelfor="opt1">Enable notifications</label>
</span>
✅ Fix: Use a semantic element instead
<spanid="label">Settings</span>
<fieldsetaria-labelledby="label">
<inputtype="checkbox"id="opt1">
<labelfor="opt1">Enable notifications</label>
</fieldset>
The aria-labelledby attribute creates a relationship between an element and the text content that labels it. It works by pointing to the id of one or more elements whose text should be used as the accessible name. When the validator reports that aria-labelledby must point to an element in the same document, it means at least one of the id values you referenced doesn't correspond to any element on the page.
This typically happens for a few reasons:
- Typo in the
id— thearia-labelledbyvalue doesn't exactly match the target element'sid(remember, IDs are case-sensitive). - The referenced element was removed — the labeling element existed at some point but was deleted or moved, and the reference wasn't updated.
- The
idexists in a different document —aria-labelledbycannot reference elements across pages, iframes, or shadow DOM boundaries. The target must be in the same document. - Dynamic content not yet rendered — the element is inserted by JavaScript after the validator parses the static HTML.
This is primarily an accessibility problem. Screen readers and other assistive technologies rely on aria-labelledby to announce meaningful labels to users. When the reference is broken, the element effectively has no accessible name, which can make it impossible for users to understand its purpose. Browsers won't throw a visible error, so the issue can go unnoticed without validation or accessibility testing.
To fix the issue, verify that every id referenced in aria-labelledby exists in the same HTML document. Double-check spelling and casing. If you reference multiple IDs (space-separated), each one must resolve to an existing element.
Examples
Incorrect — referencing a non-existent id
The aria-labelledby attribute points to "dialog-title", but no element with that id exists:
<divrole="dialog"aria-labelledby="dialog-title">
<h2id="dlg-title">Confirm deletion</h2>
<p>Are you sure you want to delete this item?</p>
</div>
Correct — matching id values
Ensure the id in the referenced element matches exactly:
<divrole="dialog"aria-labelledby="dialog-title">
<h2id="dialog-title">Confirm deletion</h2>
<p>Are you sure you want to delete this item?</p>
</div>
Incorrect — referencing multiple IDs where one is missing
When using multiple IDs, every one must be present. Here, "note-desc" doesn't exist:
<sectionaria-labelledby="note-heading note-desc">
<h3id="note-heading">Important note</h3>
<pid="note-description">Please read carefully before proceeding.</p>
</section>
Correct — all referenced IDs exist
<sectionaria-labelledby="note-heading note-description">
<h3id="note-heading">Important note</h3>
<pid="note-description">Please read carefully before proceeding.</p>
</section>
Incorrect — case mismatch
IDs are case-sensitive. "Main-Title" and "main-title" are not the same:
<navaria-labelledby="Main-Title">
<h2id="main-title">Site navigation</h2>
<ul>
<li><ahref="/">Home</a></li>
</ul>
</nav>
Correct — consistent casing
<navaria-labelledby="main-title">
<h2id="main-title">Site navigation</h2>
<ul>
<li><ahref="/">Home</a></li>
</ul>
</nav>
If you don't have a visible labeling element on the page and don't want to add one, consider using aria-label instead, which accepts a string value directly rather than referencing another element:
<navaria-label="Site navigation">
<ul>
<li><ahref="/">Home</a></li>
</ul>
</nav>
The aria-owns attribute is used to define parent-child relationships in the accessibility tree that aren't reflected in the DOM structure. For example, if a menu visually "owns" items that are placed elsewhere in the markup (perhaps for layout reasons), aria-owns tells assistive technologies that those elements should be treated as children of the owning element. The attribute accepts a space-separated list of one or more IDs.
According to the WAI-ARIA specification, each ID referenced by aria-owns must correspond to an element in the same document. If no matching element is found, the relationship is broken and the accessibility tree becomes inaccurate. This can confuse screen readers and other assistive technologies, potentially making parts of your page inaccessible or misrepresented to users who rely on them.
Common causes of this error:
- Typos in the ID value — a small misspelling like
aria-owns="drpdown-menu"instead ofaria-owns="dropdown-menu". - Referencing an element that doesn't exist — the target element was removed or never added to the page.
- Referencing an element in a different document — the target lives inside an
<iframe>or a shadow DOM, which are separate document contexts. - Dynamic content timing — the referenced element is added to the DOM by JavaScript after validation or after the
aria-ownsrelationship is evaluated.
How to fix it:
- Check that every ID in the
aria-ownsvalue exactly matches anidattribute on an element in the same document. - Look for typos or case mismatches — IDs are case-sensitive.
- If the target element is inside an
<iframe>, restructure your markup so both elements are in the same document, or use a different ARIA approach. - If the element is added dynamically, ensure it exists in the DOM before the
aria-ownsrelationship is needed by assistive technologies.
Examples
Incorrect — referencing a nonexistent ID
The element with id="dropdown-items" does not exist anywhere in the document, so the aria-owns reference is broken.
<buttonaria-owns="dropdown-items">Open Menu</button>
<!-- No element with id="dropdown-items" exists -->
Incorrect — typo in the referenced ID
The button references "menu-lst" but the actual element has id="menu-list".
<buttonaria-owns="menu-lst">Options</button>
<ulid="menu-list">
<li>Option A</li>
<li>Option B</li>
</ul>
Correct — referenced ID exists in the same document
The aria-owns value matches the id of an element present in the same page.
<buttonaria-owns="menu-list">Options</button>
<ulid="menu-list">
<li>Option A</li>
<li>Option B</li>
</ul>
Correct — multiple IDs referenced
When aria-owns lists multiple IDs, each one must have a corresponding element in the document.
<divrole="tree"aria-owns="branch1 branch2 branch3">
File Explorer
</div>
<divrole="treeitem"id="branch1">Documents</div>
<divrole="treeitem"id="branch2">Pictures</div>
<divrole="treeitem"id="branch3">Music</div>
Complete valid example
<!DOCTYPE html>
<htmllang="en">
<head>
<title>aria-owns Example</title>
</head>
<body>
<divrole="combobox"aria-expanded="true"aria-owns="suggestions">
<inputtype="text"aria-autocomplete="list">
</div>
<ulid="suggestions"role="listbox">
<lirole="option">Apple</li>
<lirole="option">Banana</li>
<lirole="option">Cherry</li>
</ul>
</body>
</html>
In this example, the combobox uses aria-owns to claim the suggestions listbox as its child in the accessibility tree, even though the <ul> is a sibling in the DOM. Because the id="suggestions" element exists in the same document, the reference is valid and assistive technologies can correctly associate the two.
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