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 <a> element is classified as interactive content, meaning it expects user interaction (clicking to navigate). The <label> element is also interactive — clicking a label activates or focuses its associated form control. When a <label> is nested inside an <a>, the browser faces an ambiguous situation: should a click navigate to the link's URL, or should it focus/activate the associated form control? The HTML specification resolves this by simply disallowing the nesting entirely.
According to the WHATWG HTML Living Standard, the content model of the <a> element is "transparent" but must not contain any interactive content. Since <label> is interactive content, it is not permitted as a descendant of <a> at any depth.
Beyond being invalid HTML, this nesting causes real problems:
- Accessibility: Screen readers may announce conflicting roles, confusing users who rely on assistive technology. The purpose of the element becomes unclear — is it a link or a form label?
- Unpredictable behavior: Different browsers may handle the click event differently, leading to inconsistent user experiences.
- Broken form association: The
<label>'sforattribute may not work as intended when the label is trapped inside a link.
The fix is straightforward: if you only need to style text inside a link, use a <span> or another non-interactive element instead of <label>. If you genuinely need both a link and a label, they should be separate, sibling elements rather than nested.
Examples
❌ Invalid: <label> inside <a>
<a href="/settings">
<label>Account Settings</label>
</a>
This triggers the validation error because <label> is interactive content nested inside <a>.
✅ Fixed: Replace <label> with <span>
<a href="/settings">
<span>Account Settings</span>
</a>
If the <label> was only used for styling purposes, a <span> with a CSS class achieves the same visual result without violating the specification.
❌ Invalid: <label> deeply nested inside <a>
<a href="/profile">
<div>
<label for="username">Edit Username</label>
</div>
</a>
The rule applies to all descendants, not just direct children. This is still invalid.
✅ Fixed: Separate the link and label
<label for="username">Edit Username</label>
<a href="/profile">View Profile</a>
When you need both a functional label and a link, keep them as siblings rather than nesting one inside the other.
✅ Fixed: Using <span> with a class for styling
<a href="/dashboard">
<span class="label-style">Dashboard</span>
</a>
.label-style {
font-weight: bold;
text-transform: uppercase;
}
This preserves any visual styling you need while keeping the HTML valid and the interaction model unambiguous.
The <label> element serves a specific purpose in HTML: it represents a caption for a form control. It can be associated with a control either through the for attribute (referencing the control's id) or by wrapping the form control inside the <label> itself. Placing a <label> inside a <button> is semantically incorrect because a button is not a form control that benefits from labeling in this way — the button's own text content or aria-label attribute already serves as its accessible name.
The HTML specification defines the content model of <button> as phrasing content, but explicitly excludes interactive content. Since <label> is classified as interactive content (it directs focus to its associated control when clicked), nesting it inside a <button> creates ambiguous behavior. When a user clicks the label, should it activate the button, or should it shift focus to the label's associated control? Browsers handle this conflict inconsistently, which leads to unpredictable user experiences.
From an accessibility standpoint, this nesting is problematic because screen readers may announce the button in a confusing way, potentially reading it as containing a label for another element. The button's accessible name should come directly from its text content, not from a nested <label>.
To fix this issue, simply replace the <label> with a <span> or plain text inside the button. If you need to style part of the button's text differently, <span> elements are perfectly valid inside buttons.
Examples
❌ Invalid: <label> inside a <button>
<button type="submit">
<label>Submit Form</label>
</button>
❌ Invalid: <label> with a for attribute inside a <button>
<button type="button">
<label for="file-input">Choose File</label>
</button>
<input type="file" id="file-input">
✅ Fixed: Use plain text inside the <button>
<button type="submit">
Submit Form
</button>
✅ Fixed: Use a <span> for styling purposes
<button type="submit">
<span class="button-text">Submit Form</span>
</button>
✅ Fixed: Separate the <label> and <button>
If you need a label to describe a button's purpose in a form, place the <label> outside the button and use the for attribute to associate it with a related input, or use aria-label on the button itself:
<label for="username">Username</label>
<input type="text" id="username">
<button type="submit" aria-label="Submit the username form">
Submit
</button>
The HTML specification defines <label> as an element whose content model allows phrasing content but explicitly excludes other <label> elements. When you nest one <label> inside another, browsers cannot determine which form control each label is meant to describe. This breaks the fundamental purpose of the <label> element—providing a clear, one-to-one association between a text description and its corresponding form control.
This issue matters for several reasons:
- Accessibility: Screen readers rely on the
<label>element to announce the purpose of form controls. Nested labels create confusion about which label text belongs to which input, making forms difficult or impossible to navigate for users of assistive technology. - Usability: Clicking a
<label>should focus or activate its associated control. Nested labels create overlapping click targets with unpredictable behavior. - Standards compliance: The WHATWG HTML living standard explicitly states that
<label>elements must not be nested, and validators will flag this as an error.
This error commonly occurs in a few situations: accidentally duplicating closing tags, wrapping a complex form group in a <label> when a <fieldset> would be more appropriate, or using a templating system that inadvertently produces nested labels.
Examples
❌ Nested labels (invalid)
<label>
Full Name
<label>
First Name
<input type="text" name="first-name">
</label>
</label>
❌ Extra closing tag causing a parser issue
A stray closing </label> tag can sometimes cause the browser's error recovery to produce unexpected nesting:
<label>Name</label></label>
<label for="email">Email</label>
While the extra </label> is the root problem here, some parsers and validators may interpret this as a nesting issue. Always ensure your opening and closing tags are properly matched.
✅ Separate labels for separate inputs
<label for="first-name">First Name</label>
<input type="text" id="first-name" name="first-name">
<label for="last-name">Last Name</label>
<input type="text" id="last-name" name="last-name">
✅ Using implicit label association (one label per input)
<label>
First Name
<input type="text" name="first-name">
</label>
<label>
Last Name
<input type="text" name="last-name">
</label>
✅ Grouping related controls with <fieldset> instead of nesting labels
If you need to group multiple labeled inputs under a shared heading, use a <fieldset> with a <legend> instead of wrapping labels inside a label:
<fieldset>
<legend>Full Name</legend>
<label for="first-name">First Name</label>
<input type="text" id="first-name" name="first-name">
<label for="last-name">Last Name</label>
<input type="text" id="last-name" name="last-name">
</fieldset>
This approach provides the grouping semantics you need while keeping each <label> correctly associated with a single form control. The <legend> serves as the group-level description, and each <label> describes its individual input—giving both sighted users and assistive technology users a clear understanding of the form structure.
The <caption> element is designed to be a brief, descriptive label for its parent <table>. According to the HTML specification, <caption> accepts flow content but explicitly forbids descendant <table> elements. This restriction exists because a table nested inside a caption creates a confusing and semantically meaningless structure — the caption is supposed to describe the table, not contain another one.
Why this is a problem
- Accessibility: Screen readers announce the
<caption>as the title of the table. A nested table inside a caption creates a confusing experience for assistive technology users, as the relationship between the tables becomes ambiguous and the caption loses its descriptive purpose. - Standards compliance: The WHATWG HTML living standard explicitly states that
<caption>must have "no<table>element descendants." Violating this produces a validation error. - Rendering inconsistencies: Browsers may handle this invalid nesting differently, leading to broken or unpredictable layouts across different environments.
How to fix it
- Remove the table from the caption. The
<caption>should contain only text and simple inline elements like<span>,<strong>,<em>, or<a>. - Place the nested table outside the parent table, either before or after it, or restructure your layout so both tables are siblings.
- If the data in the nested table is genuinely related to the caption's purpose, consider expressing it as plain text or using a different structural approach entirely.
Examples
❌ Incorrect: A table nested inside a caption
<table>
<caption>
Summary
<table>
<tr>
<td>Extra info</td>
<td>Details</td>
</tr>
</table>
</caption>
<tr>
<th>Name</th>
<th>Score</th>
</tr>
<tr>
<td>Alice</td>
<td>95</td>
</tr>
</table>
This triggers the validation error because a <table> appears as a descendant of the <caption> element.
✅ Correct: Caption contains only text, tables are separate
<table>
<caption>Summary — Extra info: Details</caption>
<tr>
<th>Name</th>
<th>Score</th>
</tr>
<tr>
<td>Alice</td>
<td>95</td>
</tr>
</table>
If the extra information truly requires its own table, place it as a sibling:
<table>
<caption>Summary</caption>
<tr>
<th>Name</th>
<th>Score</th>
</tr>
<tr>
<td>Alice</td>
<td>95</td>
</tr>
</table>
<table>
<caption>Additional details</caption>
<tr>
<td>Extra info</td>
<td>Details</td>
</tr>
</table>
✅ Correct: Caption with inline formatting only
<table>
<caption>
<strong>Quarterly Results</strong> — <em>All figures in USD</em>
</caption>
<tr>
<th>Quarter</th>
<th>Revenue</th>
</tr>
<tr>
<td>Q1</td>
<td>$1.2M</td>
</tr>
</table>
This is valid because the <caption> contains only text and inline elements (<strong>, <em>), with no <table> descendants.
The <a> element is classified as interactive content, and the HTML spec explicitly states that interactive content must not be nested inside other interactive content. A <textarea> is a form control that accepts user input—clicking, focusing, typing, and selecting text within it. When it's wrapped in a link, the browser faces a conflict: should a click focus the textarea or follow the link? Different browsers may resolve this differently, leading to inconsistent behavior.
Beyond browser inconsistency, this nesting creates serious accessibility problems. Screen readers and other assistive technologies rely on a clear, predictable document structure. When a form control is buried inside a link, the roles and interaction models overlap, making it confusing or even impossible for users relying on keyboard navigation or screen readers to interact with either element properly.
The fix depends on what you're trying to achieve. If the <textarea> and the link are logically separate, simply move them to be siblings rather than nesting one inside the other. If you need them to appear visually grouped, use a wrapper <div> or another non-interactive container element instead.
Examples
❌ Invalid: <textarea> inside an <a> element
<a href="/comments">
<textarea name="comment" rows="4" cols="40"></textarea>
</a>
This triggers the validation error because the <textarea> is a descendant of the <a> element.
✅ Valid: <textarea> and <a> as siblings
<div>
<textarea name="comment" rows="4" cols="40"></textarea>
<a href="/comments">View all comments</a>
</div>
Here, both elements live side by side inside a neutral <div>, avoiding any nesting conflict.
✅ Valid: <textarea> inside a <form> with a separate link
<form action="/submit-comment" method="post">
<label for="comment">Your comment:</label>
<textarea id="comment" name="comment" rows="4" cols="40"></textarea>
<button type="submit">Submit</button>
</form>
<a href="/comments">View all comments</a>
This is the most semantically correct approach when the textarea is part of a form—keep the form controls in a <form> and place any navigation links outside of it.
Other interactive elements to watch for
The same rule applies to other interactive content inside <a> elements. You also cannot nest <button>, <input>, <select>, <details>, or another <a> inside a link. If the validator reports a similar error for any of these elements, the fix follows the same principle: move the interactive element out of the anchor.
The charset meta tag specifies an encoding name that is not the preferred form. Use utf-8 (with a hyphen) instead of utf8.
The HTML specification requires that character encoding declarations use the preferred IANA encoding name. For the Unicode UTF-8 encoding, the preferred name is utf-8, not utf8, UTF8, or other variations. While browsers may still recognize non-preferred names, the W3C validator flags them because the WHATWG HTML standard and IANA character set registry both list utf-8 as the canonical form.
This applies to the <meta charset> declaration and, less commonly, to charset parameters in Content-Type headers or <meta http-equiv> tags.
Incorrect example
<meta charset="utf8">
Correct example
<meta charset="utf-8">
When a <select> element is marked as required, the browser needs a way to determine whether the user has made a deliberate choice. The HTML specification requires that the first <option> element act as a placeholder — a non-selectable default that represents "no choice made." For the browser's constraint validation to work correctly, this placeholder option must have an empty value attribute (value=""), or it must have no text content at all.
This requirement only applies when all three conditions are met:
- The
<select>has arequiredattribute. - The
<select>does not have amultipleattribute. - The
<select>does not have asizeattribute with a value greater than1.
In this configuration, the <select> renders as a standard single-selection dropdown, and the first <option> with an empty value serves as the "please choose" prompt. If the user submits the form without changing the selection from this placeholder, the browser will block submission and display a validation message — just as it would for an empty required text input.
Why this matters
- Form validation: Without a proper placeholder option, the browser may consider the first option as a valid selection, allowing the form to submit even when the user hasn't actively chosen anything. This defeats the purpose of
required. - Accessibility: Screen readers and assistive technologies rely on standard patterns. A placeholder option clearly communicates to all users that a selection is expected.
- Standards compliance: The WHATWG HTML specification explicitly defines this constraint, and violating it produces a validation error.
How to fix it
- Add a placeholder
<option>as the first child of the<select>, withvalue=""and descriptive prompt text (e.g., "Choose an option"). - Alternatively, if you don't want a visible placeholder, the first
<option>can have no text content at all (<option value=""></option>), though this is less user-friendly. - Another approach is to add a
sizeattribute equal to the number of options, or add themultipleattribute — but these change the visual presentation from a dropdown to a list box, so they're only appropriate if that's the desired UI.
Examples
❌ Incorrect: first option has a non-empty value
<select required>
<option value="s">Small</option>
<option value="m">Medium</option>
<option value="l">Large</option>
</select>
Here, "Small" is preselected and has a non-empty value. The browser treats it as a valid choice, so required validation never triggers — the form can be submitted without the user making an active decision.
❌ Incorrect: placeholder option has a non-empty value
<select required>
<option value="none">Choose a size</option>
<option value="s">Small</option>
<option value="m">Medium</option>
<option value="l">Large</option>
</select>
The first option looks like a placeholder, but its value is "none" rather than empty. The validator flags this because the browser considers "none" a valid value.
✅ Correct: placeholder option with an empty value
<select required>
<option value="">Choose a size</option>
<option value="s">Small</option>
<option value="m">Medium</option>
<option value="l">Large</option>
</select>
The first <option> has value="" and serves as a clear prompt. If the user doesn't select a different option, form validation will prevent submission.
✅ Correct: placeholder option with no text content
<select required>
<option value=""></option>
<option value="s">Small</option>
<option value="m">Medium</option>
<option value="l">Large</option>
</select>
This also satisfies the constraint, though it may appear as a blank entry in the dropdown. It can work in cases where a <label> already makes the expected action clear.
✅ Correct: using a size attribute to avoid the requirement
<select required size="3">
<option value="s">Small</option>
<option value="m">Medium</option>
<option value="l">Large</option>
</select>
By adding size="3" (equal to the number of options), the <select> renders as a list box rather than a dropdown. The placeholder requirement no longer applies because no option is implicitly preselected — the user must click to choose. Note that this changes the visual appearance significantly.
The HTML specification requires that every id attribute value must be unique within a document. When the validator encounters the same id on two or more elements, it raises an error on the second (and subsequent) occurrences, along with a companion message — "The first occurrence of ID 'X' was here" — pointing to the original element. This companion message helps you quickly compare both locations and decide which one to rename or remove.
Why Duplicate IDs Are a Problem
Accessibility: Screen readers and other assistive technologies rely on unique IDs to associate <label> elements with form controls, to navigate ARIA relationships like aria-labelledby and aria-describedby, and to jump to page landmarks. When IDs are duplicated, these associations break, leaving users confused or unable to interact with the page properly.
JavaScript behavior: Methods like document.getElementById() return only the first matching element. If you intend to target the second element with a duplicated ID, your code will silently operate on the wrong one. This can lead to bugs that are difficult to track down.
CSS specificity: While #my-id selectors will style all elements with that ID in most browsers, this is non-standard behavior. Relying on it leads to fragile, unpredictable styling.
Fragment navigation: Links using href="#section" scroll to the first element with that ID. Duplicate IDs make it impossible to link to the second occurrence.
How to Fix It
- Identify the duplicates. The validator tells you the line number of the first occurrence and the line number of the duplicate. Compare both elements.
- Rename one of the IDs to something unique and descriptive. Update any corresponding references (labels, ARIA attributes, JavaScript selectors, CSS rules, and anchor links).
- Use
classinstead ofidwhen you need to apply the same style or behavior to multiple elements. Classes are designed to be reused; IDs are not.
Examples
❌ Duplicate IDs trigger the error
<div id="product-card">
<h2>Widget A</h2>
<p>A useful widget.</p>
</div>
<div id="product-card">
<h2>Widget B</h2>
<p>Another useful widget.</p>
</div>
The validator will report an error on the second div and display the message "The first occurrence of ID 'product-card' was here" pointing to the first div.
✅ Fixed: Give each element a unique ID
<div id="product-card-a">
<h2>Widget A</h2>
<p>A useful widget.</p>
</div>
<div id="product-card-b">
<h2>Widget B</h2>
<p>Another useful widget.</p>
</div>
✅ Fixed: Use a class for shared styling or behavior
If both elements don't need to be individually targeted, replace the id with a class:
<div class="product-card">
<h2>Widget A</h2>
<p>A useful widget.</p>
</div>
<div class="product-card">
<h2>Widget B</h2>
<p>Another useful widget.</p>
</div>
❌ Duplicate IDs breaking a label association
<label for="email">Email</label>
<input type="email" id="email" name="primary-email">
<label for="email">Backup Email</label>
<input type="email" id="email" name="backup-email">
Both labels point to for="email", but only the first input will be associated. The second label effectively has a broken link.
✅ Fixed: Unique IDs for each form control
<label for="primary-email">Email</label>
<input type="email" id="primary-email" name="primary-email">
<label for="backup-email">Backup Email</label>
<input type="email" id="backup-email" name="backup-email">
Now each <label> correctly associates with its own <input>, and both assistive technologies and JavaScript can target each field reliably.
The <font> element was originally introduced to give authors control over text rendering directly in markup. A typical usage looked like <font face="Arial" size="3" color="red">. While browsers still render this element for backward compatibility, it has been obsolete since HTML5 and will trigger a validation error. The W3C validator flags it because it violates the principle of separation of concerns: HTML should define the structure and meaning of content, while CSS should handle its visual presentation.
Using <font> causes several practical problems:
- Maintainability: Styling scattered across
<font>tags throughout your HTML is extremely difficult to update. Changing a color scheme could mean editing hundreds of elements instead of a single CSS rule. - Accessibility: The
<font>element carries no semantic meaning. Screen readers and other assistive technologies gain nothing from it, and its presence can clutter the document structure. - Consistency: CSS enables you to define styles in one place and apply them uniformly across your entire site using classes, selectors, or external stylesheets.
- Standards compliance: Using obsolete elements means your HTML does not conform to the current specification, which can lead to unexpected rendering in future browser versions.
To fix this issue, remove every <font> element and replace its visual effects with equivalent CSS properties. The three attributes of <font> map directly to CSS:
<font> attribute | CSS equivalent |
|---|---|
color | color |
size | font-size |
face | font-family |
You can apply CSS as inline styles for quick fixes, but using a <style> block or an external stylesheet with classes is the preferred approach for any real project.
Examples
Incorrect: using the obsolete <font> element
<p>
<font face="Arial" size="4" color="blue">Welcome to my website</font>
</p>
This triggers the validator error: The "font" element is obsolete. Use CSS instead.
Fix with inline styles
If you need a quick, direct replacement:
<p style="font-family: Arial, sans-serif; font-size: 18px; color: blue;">
Welcome to my website
</p>
Fix with a CSS class (recommended)
Using a class keeps your HTML clean and makes styles reusable:
<style>
.welcome-text {
font-family: Arial, sans-serif;
font-size: 18px;
color: blue;
}
</style>
<p class="welcome-text">Welcome to my website</p>
Nested <font> elements replaced with CSS
Old markup sometimes used multiple nested <font> tags:
<!-- Obsolete -->
<p>
<font color="red" size="5">
Important:
<font face="Courier">code goes here</font>
</font>
</p>
The correct approach uses <span> elements or semantic tags with CSS classes:
<style>
.alert-heading {
color: red;
font-size: 24px;
}
.code-snippet {
font-family: Courier, monospace;
}
</style>
<p>
<span class="alert-heading">
Important:
<span class="code-snippet">code goes here</span>
</span>
</p>
If the text carries a specific meaning — such as marking something as important or representing code — consider using semantic HTML elements like <strong>, <em>, or <code> alongside your CSS:
<style>
.alert-heading {
color: red;
font-size: 24px;
}
</style>
<p class="alert-heading">
<strong>Important:</strong>
<code>code goes here</code>
</p>
This approach gives you full control over appearance through CSS while keeping your HTML meaningful, accessible, and standards-compliant.
Many HTML elements come with built-in (implicit) ARIA roles that browsers and assistive technologies already recognize. The <form> element natively maps to the form ARIA role, meaning screen readers and other tools already understand it as a form landmark without any extra attributes. When you explicitly add role="form" to a <form> element, you're telling the browser something it already knows.
This redundancy is problematic for several reasons:
- Code clarity: Unnecessary attributes make your HTML harder to read and maintain. Other developers may wonder if the explicit role is there to override something or if it serves a special purpose.
- Misleading intent: Explicit ARIA roles are typically reserved for cases where you need to override or supplement the default semantics of an element. Using them unnecessarily can signal to future maintainers that something unusual is happening when it isn't.
- ARIA best practices: The first rule of ARIA is "do not use ARIA if you can use a native HTML element or attribute with the semantics and behavior you require." Adding redundant ARIA roles goes against this principle.
It's worth noting that the <form> element's implicit form role only exposes it as a landmark when the form has an accessible name (e.g., via aria-label or aria-labelledby). If you need your form to appear as a landmark region, provide an accessible name rather than adding a redundant role.
To fix this issue, simply remove role="form" from any <form> element. If you want the form to function as a named landmark for assistive technology users, add an accessible name instead.
Examples
❌ Incorrect: redundant role="form"
<form role="form" action="/subscribe" method="post">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
This triggers the validator warning because role="form" duplicates the element's implicit role.
✅ Correct: no explicit role
<form action="/subscribe" method="post">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
The <form> element already communicates its role natively. No ARIA attribute is needed.
✅ Correct: form with an accessible name for landmark navigation
<form action="/subscribe" method="post" aria-label="Newsletter subscription">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
If you want the form to be discoverable as a named landmark by screen reader users, provide an aria-label or aria-labelledby attribute — not a redundant role.
Other elements with implicit roles
The same principle applies to many other HTML elements. Avoid adding redundant roles like these:
<!-- ❌ Redundant roles -->
<nav role="navigation">...</nav>
<main role="main">...</main>
<header role="banner">...</header>
<footer role="contentinfo">...</footer>
<button role="button">Click me</button>
<!-- ✅ Let native semantics do the work -->
<nav>...</nav>
<main>...</main>
<header>...</header>
<footer>...</footer>
<button>Click me</button>
Trust the native semantics of HTML elements. Only use explicit ARIA roles when you genuinely need to change or supplement an element's default behavior.
The <gcse:search> element is a custom tag from Google's Programmable Search Engine widget and is not part of any HTML specification.
Google's Programmable Search Engine (formerly Custom Search Engine) offers two ways to drop a search box onto a page. The older one uses namespaced tags such as <gcse:search>, <gcse:searchbox>, and <gcse:searchresults>, which the loaded cse.js script finds and replaces with the real widget. Browsers do not recognize the gcse: prefix, so they treat the tag as an unknown element, and the W3C validator rejects it because no such element exists in HTML.
Google supports an equivalent form that is valid HTML: a standard <div> carrying a gcse- class, like <div class="gcse-search">. The script reads the class instead of a custom tag name and renders the same search box. Switching to the class form clears the validation error without changing how the widget works.
HTML examples
Invalid: namespaced <gcse:search> element
<script async src="https://cse.google.com/cse.js?cx=YOUR_SEARCH_ENGINE_ID"></script>
<gcse:search></gcse:search>
Valid: div with the gcse-search class
<script async src="https://cse.google.com/cse.js?cx=YOUR_SEARCH_ENGINE_ID"></script>
<div class="gcse-search"></div>
The same applies to the other Programmable Search tags: replace <gcse:searchbox> with <div class="gcse-searchbox"></div> and <gcse:searchresults> with <div class="gcse-searchresults"></div>. Each gcse: element has a matching gcse- class that produces the same result and passes validation.
Many HTML elements come with built-in ARIA roles that assistive technologies already recognize. The <fieldset> element is one of these — its implicit role is group, which tells screen readers that the contained form controls are related. When you add role="group" to a <fieldset>, you're telling the browser something it already knows.
This redundancy matters for a few reasons:
- Code cleanliness: Unnecessary attributes add clutter, making your markup harder to read and maintain.
- ARIA best practices: The first rule of ARIA is "If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so." Adding
role="group"to<fieldset>violates this principle in spirit — it suggests the developer may not understand the element's native semantics. - Potential confusion: Explicitly setting roles that match the default can mislead other developers into thinking the role is doing something special, or that removing it would change behavior.
This same principle applies to other elements with implicit roles, such as role="navigation" on <nav>, role="banner" on <header>, or role="button" on <button>. If the element already carries the semantic meaning natively, there's no need to duplicate it with an explicit ARIA role.
To fix this, simply remove the role="group" attribute from the <fieldset> element. No replacement is needed — the browser and assistive technologies will continue to treat the <fieldset> as a group automatically.
Examples
Incorrect: redundant role="group" on <fieldset>
<form>
<fieldset role="group">
<legend>Shipping Address</legend>
<label for="street">Street:</label>
<input type="text" id="street" name="street">
<label for="city">City:</label>
<input type="text" id="city" name="city">
</fieldset>
</form>
The validator will report that the group role is unnecessary for the <fieldset> element.
Correct: <fieldset> without explicit role
<form>
<fieldset>
<legend>Shipping Address</legend>
<label for="street">Street:</label>
<input type="text" id="street" name="street">
<label for="city">City:</label>
<input type="text" id="city" name="city">
</fieldset>
</form>
The <fieldset> element inherently communicates the group role to assistive technologies, so no ARIA attribute is needed.
When role on <fieldset> is appropriate
There are cases where you might legitimately set a different role on a <fieldset> — for example, role="radiogroup" when the fieldset contains a set of related radio buttons and you want to convey more specific semantics:
<form>
<fieldset role="radiogroup" aria-labelledby="color-legend">
<legend id="color-legend">Favorite Color</legend>
<label><input type="radio" name="color" value="red"> Red</label>
<label><input type="radio" name="color" value="blue"> Blue</label>
<label><input type="radio" name="color" value="green"> Green</label>
</fieldset>
</form>
This is valid because radiogroup is a different role that provides more specific meaning than the default group. The validator only warns when the explicit role matches the element's implicit role.
The headers attribute creates explicit associations between data cells (td) and header cells (th) in complex tables. This is especially important for tables with irregular structures—such as those with merged cells or multiple header levels—where the browser cannot automatically determine which headers apply to which data cells.
When the validator reports this error, it means one or more IDs referenced in a td's headers attribute cannot be matched to any th element with that id in the same table. Common causes include:
- Typos — A small misspelling in either the
headersvalue or thethelement'sid. - Missing
id— Thethelement exists but doesn't have anidattribute assigned. - Removed or renamed headers — The
thwas deleted or itsidwas changed during refactoring, but thetdstill references the old value. - Cross-table references — The
thwith the referencedidexists in a different<table>, which is not allowed.
Why this matters
This issue directly impacts accessibility. Screen readers use the headers attribute to announce which header cells are associated with a data cell. When a referenced ID doesn't resolve to a th in the same table, assistive technology cannot provide this context, making the table confusing or unusable for users who rely on it. Broken headers references also indicate invalid HTML according to the WHATWG HTML specification, which requires that each token in the headers attribute match the id of a th cell in the same table.
How to fix it
- Locate the
tdelement flagged by the validator and note the ID it references. - Search the same
<table>for athelement with a matchingid. - If the
thexists but has noidor a differentid, add or correct theidattribute so it matches. - If the
thwas removed, either restore it or remove theheadersattribute from thetd. - Double-check for case sensitivity — HTML
idvalues are case-sensitive, soheaders="Name"does not matchid="name".
Examples
Incorrect: headers references a non-existent ID
The first td references "product", but no th has id="product". The second th has id="cost", but the second td references "price" — a mismatch.
<table>
<tr>
<th>Product</th>
<th id="cost">Price</th>
</tr>
<tr>
<td headers="product">Widget</td>
<td headers="price">$9.99</td>
</tr>
</table>
Correct: each headers value matches a th with the same id
<table>
<tr>
<th id="product">Product</th>
<th id="cost">Price</th>
</tr>
<tr>
<td headers="product">Widget</td>
<td headers="cost">$9.99</td>
</tr>
</table>
Correct: multiple headers on a single td
In complex tables, a data cell may relate to more than one header. List multiple IDs separated by spaces — each one must correspond to a th in the same table.
<table>
<tr>
<th id="region" rowspan="2">Region</th>
<th id="q1" colspan="2">Q1</th>
</tr>
<tr>
<th id="sales">Sales</th>
<th id="returns">Returns</th>
</tr>
<tr>
<td headers="region">North</td>
<td headers="q1 sales">1200</td>
<td headers="q1 returns">45</td>
</tr>
</table>
Tip: simple tables may not need headers at all
For straightforward tables with a single row of column headers, browsers and screen readers can infer the associations automatically. In those cases, you can omit the headers attribute entirely and avoid this class of error:
<table>
<tr>
<th>Product</th>
<th>Price</th>
</tr>
<tr>
<td>Widget</td>
<td>$9.99</td>
</tr>
</table>
Reserve the headers attribute for complex tables where automatic association is insufficient — such as tables with cells that span multiple rows or columns, or tables with headers in both rows and columns.
HTML heading elements (<h1> through <h6>) have built-in semantic meaning that browsers and assistive technologies already understand. According to the WAI-ARIA specification, each of these elements carries an implicit heading role with a corresponding aria-level — <h1> has aria-level="1", <h2> has aria-level="2", and so on. When you explicitly add role="heading" to one of these elements, you're telling the browser something it already knows, which clutters your markup without providing any benefit.
This pattern is part of a broader principle in ARIA authoring known as the first rule of ARIA: don't use ARIA when a native HTML element already provides the semantics you need. Redundant ARIA roles can cause confusion for developers maintaining the code, as it suggests that the role might be necessary or that the element might not otherwise be recognized as a heading. In some edge cases, adding an explicit aria-level that doesn't match the heading level (e.g., aria-level="3" on an <h1>) can create conflicting information for screen readers, leading to an inconsistent experience for users of assistive technologies.
The role="heading" attribute is designed for situations where you need to give heading semantics to a non-heading element, such as a <div> or <span>. In those cases, you must also include the aria-level attribute to specify the heading's level. However, whenever possible, using native heading elements is always preferred over this ARIA-based approach.
How to fix it
- Remove
role="heading"from any<h1>through<h6>element. - Remove
aria-levelif it was added alongside the redundant role and matches the heading's native level. - If you genuinely need a non-standard element to act as a heading, use
role="heading"witharia-levelon that element instead — but prefer native heading elements whenever possible.
Examples
❌ Redundant role on a native heading
<h1 role="heading" aria-level="1">Welcome to My Site</h1>
<h2 role="heading">About Us</h2>
<h3 role="heading" aria-level="3">Our Mission</h3>
All three headings will trigger the validator warning. The role="heading" and aria-level attributes are completely unnecessary here because the elements already convey this information natively.
✅ Native headings without redundant roles
<h1>Welcome to My Site</h1>
<h2>About Us</h2>
<h3>Our Mission</h3>
Simply removing the redundant attributes resolves the issue while preserving full accessibility.
✅ Correct use of the heading role on a non-heading element
In rare cases where you cannot use a native heading element, the heading role is appropriate on a generic element:
<div role="heading" aria-level="2">Section Title</div>
This tells assistive technologies to treat the <div> as a level-2 heading. Note that aria-level is required here since a <div> has no implicit heading level. That said, using a native <h2> is always the better choice:
<h2>Section Title</h2>
❌ Conflicting aria-level on a native heading
Be especially careful with this anti-pattern, where the explicit level contradicts the element:
<h1 role="heading" aria-level="3">Page Title</h1>
This sends mixed signals — the element is an <h1> but claims to be level 3. Screen readers may behave unpredictably. If you need a level-3 heading, use <h3>:
<h3>Page Title</h3>
A heading level has been skipped in the document's heading hierarchy, such as jumping from an <h2> directly to an <h4>.
HTML headings (<h1> through <h6>) form a hierarchical outline of the document. Screen readers and other assistive technologies use this hierarchy to help users navigate content. When a level is skipped, the outline becomes ambiguous: does the <h4> after an <h2> belong to a missing <h3> section, or is it a direct subsection of the <h2>? Users who navigate by heading level may assume content is missing.
The rule is straightforward: after an <h1>, the next heading should be <h2>. After an <h2>, use <h3>, and so on. You can go back up the hierarchy at any time (an <h2> after an <h4> is fine, because it starts a new section), but you should not skip down.
If you are using a heading level purely for its visual size, use CSS instead. Apply the correct semantic heading level and style it however you want.
Example with skipped heading level
<h1>Recipe book</h1>
<h2>Desserts</h2>
<h4>Chocolate cake</h4>
The jump from <h2> to <h4> skips the <h3> level.
Fixed heading hierarchy
<h1>Recipe book</h1>
<h2>Desserts</h2>
<h3>Chocolate cake</h3>
If the <h4> was chosen for its smaller font size, apply CSS to the <h3> instead:
<style>
.small-heading {
font-size: 1rem;
}
</style>
<h1>Recipe book</h1>
<h2>Desserts</h2>
<h3 class="small-heading">Chocolate cake</h3>
The <icon> element does not exist in HTML. No version of the HTML specification defines it, and browsers do not recognize it.
This error appears when markup includes <icon> as if it were a standard HTML element. It is not. Browsers will treat it as an unknown inline element with no default behavior or semantics. To display icons, use an <img> element, an <svg> element, or a <span> with CSS-applied background images or icon fonts.
If the intent is to define a favicon (the small icon shown in browser tabs), the correct approach is a <link> element inside <head> with rel="icon".
HTML examples
Invalid: using the <icon> element
<head>
<title>My page</title>
<icon src="favicon.png"></icon>
</head>
Valid: using a <link> element for a favicon
<head>
<title>My page</title>
<link rel="icon" href="favicon.png" type="image/png">
</head>
Valid: displaying an icon inline with <img>
<p>
<img src="star.svg" alt="Star" width="16" height="16"> Favorite
</p>
Valid: displaying an icon inline with <span> and CSS
<p>
<span class="icon icon-star" aria-hidden="true"></span> Favorite
</p>
Every HTML element has an implicit ARIA role defined by the HTML specification. The <img> element's implicit role is img, which means assistive technologies like screen readers already recognize it as an image without any additional ARIA attributes. Adding role="img" explicitly doesn't change behavior — it just adds unnecessary noise to your markup and signals that the author may not understand how native semantics work.
The W3C validator flags this because it violates the first rule of ARIA: don't use ARIA if you can use a native HTML element or attribute that already has the semantics you need. Redundant roles clutter your code, make maintenance harder, and can confuse other developers into thinking the role is there for a specific reason.
The role="img" attribute is genuinely useful in other contexts — for example, when you want to group multiple elements together and have them treated as a single image by assistive technologies. A <div> or <span> has no implicit img role, so adding role="img" to a container is meaningful and appropriate.
How to fix it
Simply remove the role="img" attribute from any <img> element. The image semantics are already built in. Make sure you still provide a meaningful alt attribute for accessibility.
Examples
❌ Redundant role on <img>
<img src="photo.jpg" alt="A sunset over the ocean" role="img">
The validator will warn: The "img" role is unnecessary for element "img".
✅ Fixed: Remove the redundant role
<img src="photo.jpg" alt="A sunset over the ocean">
No explicit role is needed. The browser already communicates this element as an image.
✅ Legitimate use of role="img" on a non-image element
The role="img" attribute is appropriate when applied to a container that groups multiple elements into a single conceptual image:
<div role="img" aria-label="Star rating: 4 out of 5">
<span>⭐</span>
<span>⭐</span>
<span>⭐</span>
<span>⭐</span>
<span>☆</span>
</div>
Here, the <div> has no inherent image semantics, so role="img" is meaningful — it tells assistive technologies to treat the entire group as a single image described by the aria-label.
✅ Another legitimate use: CSS background image with role="img"
<div role="img" aria-label="Company logo" class="logo-background"></div>
Since a <div> styled with a CSS background image has no image semantics, role="img" paired with aria-label ensures the visual content is accessible.
The inputmode attribute is a global attribute that can be applied to any element that is editable, including <input> elements and elements with contenteditable. It tells the browser which type of virtual keyboard to present—for example, a numeric keypad, a telephone dialpad, or a URL-optimized keyboard. This is particularly useful on mobile devices where the on-screen keyboard can be tailored to the expected input.
The W3C validator raises this as an informational warning, not an error. The inputmode attribute is part of the WHATWG HTML Living Standard and is valid HTML. However, the validator flags it because browser support, while now quite broad, has historically been inconsistent. Older versions of Safari, Firefox, and some less common browsers lacked support for certain inputmode values. When inputmode is not recognized, the browser simply ignores it and shows the default keyboard—so it degrades gracefully and won't break your page.
The valid values for inputmode are:
none— No virtual keyboard; useful when the page provides its own input interface.text— Standard text keyboard (the default).decimal— Numeric keyboard with a decimal separator, ideal for fractional numbers.numeric— Numeric keyboard without a decimal separator, ideal for PINs or zip codes.tel— Telephone keypad layout with digits 0–9,*, and#.search— A keyboard optimized for search input, which may include a "Search" or "Go" button.email— A keyboard optimized for email entry, typically including@and.prominently.url— A keyboard optimized for URL entry, typically including/and.com.
It's important to understand the difference between inputmode and the type attribute. The type attribute on <input> defines the semantics and validation behavior of the field (e.g., type="email" validates that the value looks like an email address). The inputmode attribute only affects the virtual keyboard hint and has no impact on validation or semantics. This makes inputmode especially useful when you need a specific keyboard but the field type doesn't match—for example, a numeric PIN field that should remain type="text" to avoid the spinner controls that come with type="number".
How to fix it
Since this is a warning rather than an error, no fix is strictly required. However, you should:
- Test on your target browsers and devices to confirm the virtual keyboard behaves as expected.
- Pair
inputmodewith the appropriatetypeandpatternattributes to ensure proper validation and semantics, sinceinputmodealone does not enforce any input constraints. - Accept graceful degradation — in browsers that don't support
inputmode, users will simply see the default keyboard, which is still functional.
There is no widely adopted polyfill for inputmode because it controls a browser-native UI feature (the virtual keyboard) that JavaScript cannot directly replicate. The best strategy is to treat it as a progressive enhancement.
Examples
Using inputmode for a numeric PIN field
This example triggers the validator warning. The code is valid, but the validator advises caution:
<label for="pin">Enter your PIN:</label>
<input id="pin" type="text" inputmode="numeric" pattern="[0-9]*">
Here, type="text" keeps the field free of number-spinner controls, inputmode="numeric" requests a numeric keypad on mobile, and pattern="[0-9]*" provides client-side validation. This combination is the recommended approach for PIN or verification code fields.
Using inputmode for a currency amount
<label for="amount">Amount ($):</label>
<input id="amount" type="text" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}">
The decimal value displays a numeric keyboard that includes a decimal point, which is ideal for monetary values.
Falling back to type when inputmode is unnecessary
If the semantic input type already provides the correct keyboard, you don't need inputmode at all:
<label for="email">Email address:</label>
<input id="email" type="email">
<label for="phone">Phone number:</label>
<input id="phone" type="tel">
<label for="website">Website:</label>
<input id="website" type="url">
Using the appropriate type gives you both the optimized keyboard and built-in browser validation, making inputmode redundant in these cases.
Using inputmode on a contenteditable element
The inputmode attribute also works on non-input elements that accept user input:
<div contenteditable="true" inputmode="numeric">
Enter a number here
</div>
This is one scenario where inputmode is especially valuable, since contenteditable elements don't have a type attribute to influence the keyboard.
Microdata is an HTML specification that lets you embed machine-readable data into your content using three main attributes: itemscope, itemtype, and itemprop. The itemscope attribute creates a new item (a group of name-value pairs), itemtype specifies what kind of thing the item is (using a vocabulary URL like Schema.org), and itemprop defines individual properties within that item. These attributes work together — itemprop only makes sense in the context of an itemscope.
When the validator encounters an itemprop attribute on an element that isn't a descendant of any element with itemscope, it has no way to associate that property with an item. The property is essentially orphaned. This is a problem for several reasons:
- Search engines can't use the data. Structured data consumers like Google, Bing, and other crawlers rely on the
itemscope/itemprophierarchy to understand your content. An orphaneditempropis ignored or misinterpreted. - Standards compliance. The WHATWG HTML living standard requires that an element with
itempropmust be a property of an item — meaning it must have an ancestor withitemscope, or be explicitly referenced via theitemrefattribute on anitemscopeelement. - Maintenance issues. Orphaned
itempropattributes suggest that surrounding markup was refactored and the microdata structure was accidentally broken.
The most common causes of this error are:
- Missing
itemscope— You addeditempropattributes but forgot to define the containing item withitemscope. - Moved elements — An element with
itempropwas moved outside of its originalitemscopecontainer during a refactor. - Copy-paste errors — You copied a snippet that included
itempropbut not the parentitemscope.
To fix the issue, either wrap the itemprop elements inside an itemscope container, use itemref to associate distant properties with an item, or remove the itemprop attribute if structured data is not intended.
Examples
Incorrect: itemprop without itemscope
This triggers the validation error because there is no itemscope ancestor:
<div>
<p>My name is <span itemprop="name">Liza</span>.</p>
</div>
Correct: itemprop inside an itemscope container
Adding itemscope (and optionally itemtype) to an ancestor element fixes the issue:
<div itemscope itemtype="https://schema.org/Person">
<p>My name is <span itemprop="name">Liza</span>.</p>
</div>
Correct: nested items with their own scope
When an item contains a sub-item, the nested item needs its own itemscope:
<div itemscope itemtype="https://schema.org/Person">
<p itemprop="name">Liza</p>
<div itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
<span itemprop="addressLocality">Portland</span>,
<span itemprop="addressRegion">OR</span>
</div>
</div>
Correct: using itemref for properties outside the scope
If you can't restructure your HTML to nest itemprop inside itemscope, use itemref to reference elements by their id:
<div itemscope itemtype="https://schema.org/Person" itemref="user-name"></div>
<p id="user-name">
My name is <span itemprop="name">Liza</span>.
</p>
In this case, the itemprop="name" element is not a descendant of the itemscope element, but the itemref="user-name" attribute explicitly pulls the referenced element's tree into the item, making it valid.
Incorrect: scope broken after refactoring
A common real-world scenario where the error appears after restructuring:
<div itemscope itemtype="https://schema.org/Product">
<span itemprop="name">Widget</span>
</div>
<!-- This was moved out of the div above -->
<span itemprop="price">9.99</span>
Fix this by either moving the element back inside the itemscope container, using itemref, or removing the orphaned itemprop.
The itemtype and itemscope attributes are part of the HTML Microdata specification, which allows you to embed structured, machine-readable data into your HTML. The itemscope attribute creates a new item — it defines a scope within which properties (via itemprop) are associated. The itemtype attribute then specifies a vocabulary URL (typically from Schema.org) that describes what kind of item it is.
According to the WHATWG HTML Living Standard, itemtype has no meaning without itemscope. The itemscope attribute is what establishes the element as a microdata item container. Without it, itemtype has nothing to qualify — there is no item to assign a type to. This is why the spec requires itemscope to be present whenever itemtype is used.
Getting this wrong matters for several reasons:
- Structured data won't work. Search engines like Google rely on valid microdata to generate rich results (e.g., star ratings, event details, product prices). Invalid markup means your structured data will be silently ignored.
- Standards compliance. Using
itemtypewithoutitemscopeviolates the HTML specification, and validators will flag it as an error. - Maintainability. Other developers (or your future self) may assume the microdata is functioning correctly when it isn't.
To fix this issue, you have two options:
- Add
itemscopeto the element — this is the correct fix if you intend to use microdata. - Remove
itemtype— this is appropriate if you don't actually need structured data on that element.
Examples
Incorrect — itemtype without itemscope
This triggers the validation error because itemscope is missing:
<div itemtype="https://schema.org/Person">
<p><span itemprop="name">Liza Jane</span></p>
<p><span itemprop="email">liza.jane@example.com</span></p>
</div>
Correct — adding itemscope alongside itemtype
Adding the itemscope attribute establishes the element as a microdata item, making itemtype valid:
<div itemscope itemtype="https://schema.org/Person">
<p><span itemprop="name">Liza Jane</span></p>
<p><span itemprop="email">liza.jane@example.com</span></p>
</div>
Here, itemscope tells parsers that this div contains a microdata item, and itemtype="https://schema.org/Person" specifies that the item is a Person with properties like name and email.
Correct — removing itemtype when structured data isn't needed
If you don't need typed structured data, simply remove the itemtype attribute. You can still use itemscope on its own to create an untyped item, or remove both attributes entirely:
<div>
<p><span>Liza Jane</span></p>
<p><span>liza.jane@example.com</span></p>
</div>
Correct — nested items with itemscope and itemtype
When nesting microdata items, each level that uses itemtype must also have itemscope:
<div itemscope itemtype="https://schema.org/Organization">
<span itemprop="name">Acme Corp</span>
<div itemprop="founder" itemscope itemtype="https://schema.org/Person">
<span itemprop="name">Liza Jane</span>
</div>
</div>
Notice that both the outer div (the Organization) and the inner div (the Person) have itemscope paired with their respective itemtype values. Omitting itemscope from either element would trigger the validation error.
The HTML specification defines the <label> element as a caption for a single form control. When you place multiple labelable elements inside one <label>, the browser cannot determine which control the label text is associated with. This creates ambiguity for assistive technologies like screen readers, which rely on a clear one-to-one relationship between labels and their controls to announce form fields correctly. It also breaks the click-to-focus behavior — clicking the label text should focus or activate the associated control, but with multiple controls nested inside, the intended target is unclear.
Labelable elements are specifically: <button>, <input> (except type="hidden"), <meter>, <output>, <progress>, <select>, and <textarea>. If any combination of two or more of these appears as descendants of a single <label>, the validator will flag the error.
A common scenario that triggers this is when developers try to group related fields — like a first name and last name — inside one label, or when they nest a button alongside an input within a label for styling convenience.
How to Fix It
- Use one
<label>per control. Wrap each labelable element in its own<label>, or use theforattribute to associate a<label>with a specific control'sid. - Use a container element for grouping. If you need to visually group related controls, use a
<fieldset>with a<legend>instead of a single<label>.
Examples
❌ Incorrect: Two inputs inside one label
<label>
Name
<input type="text" name="first" placeholder="First">
<input type="text" name="last" placeholder="Last">
</label>
This is invalid because the <label> contains two <input> descendants.
✅ Fixed: Separate labels for each input
<label for="first">First name</label>
<input type="text" id="first" name="first">
<label for="last">Last name</label>
<input type="text" id="last" name="last">
✅ Fixed: Using a fieldset to group related controls
<fieldset>
<legend>Name</legend>
<label>
First
<input type="text" name="first">
</label>
<label>
Last
<input type="text" name="last">
</label>
</fieldset>
❌ Incorrect: A select and a button inside one label
<label>
Pick your age
<select name="age">
<option>Young</option>
<option>Old</option>
</select>
<button type="button">Help</button>
</label>
✅ Fixed: Button moved outside the label
<label>
Pick your age
<select name="age">
<option>Young</option>
<option>Old</option>
</select>
</label>
<button type="button">Help</button>
✅ Correct: One control inside a label (implicit association)
<label>
Age
<select id="age" name="age">
<option>Young</option>
<option>Old</option>
</select>
</label>
This is valid because the <label> contains exactly one labelable descendant — the <select> element. The association between the label text and the control is implicit and clear to both browsers and assistive technologies.
The language attribute on the <script> element has been obsolete since HTML4 and should be removed.
Early versions of HTML used language="JavaScript" to specify the scripting language. Modern HTML defaults to JavaScript, so neither language nor type is required in most cases. The type attribute replaced language long ago, but even type="text/javascript" is unnecessary now because all browsers treat scripts as JavaScript by default.
If you do need to specify a non-JavaScript type (such as type="module" or type="application/json"), use the type attribute. Otherwise, omit both attributes entirely.
Examples
Invalid: using the obsolete language attribute
<script language="JavaScript">
console.log("Hello");
</script>
Valid: no attribute needed for plain JavaScript
<script>
console.log("Hello");
</script>
Valid: using type when a specific type is needed
<script type="module">
import { greet } from "./greet.js";
greet();
</script>
The language attribute was used in early HTML to specify the scripting language of a <script> block, typically set to values like "JavaScript" or "VBScript". It was deprecated in HTML 4.01 (in favor of the type attribute) and is now fully obsolete in the HTML Living Standard. While browsers still recognize it for backward compatibility, it serves no functional purpose and triggers a validation warning.
The <script> element accepts several standard attributes, but the two most common are type and src. The type attribute specifies the MIME type or module type of the script (e.g., "module" or "application/json"), and src points to an external script file. When writing standard JavaScript, you can omit type entirely because "text/javascript" is the default. The language attribute, however, should always be removed — it is not a valid substitute for type and has no effect in modern browsers.
Why this matters
- Standards compliance: Using obsolete attributes means your HTML does not conform to the current HTML specification. This can cause validation errors that obscure more important issues in your markup.
- Code clarity: The
languageattribute is misleading to developers who may not realize it's non-functional. Removing it keeps your code clean and easier to maintain. - Future-proofing: While browsers currently tolerate the attribute, there is no guarantee they will continue to do so indefinitely. Relying on obsolete features is a maintenance risk.
How to fix it
Simply remove the language attribute from your <script> elements. If you're using JavaScript (the vast majority of cases), no replacement is needed. If you need to specify a non-default type, use the type attribute instead.
Examples
❌ Obsolete: using the language attribute
<script language="JavaScript">
console.log("Hello, world!");
</script>
<script language="JavaScript" src="app.js"></script>
✅ Fixed: attribute removed
For inline JavaScript, simply omit the attribute:
<script>
console.log("Hello, world!");
</script>
For external scripts, only src is needed:
<script src="app.js"></script>
✅ Using the type attribute when needed
If you need to specify a script type — for example, an ES module or a data block — use the standard type attribute:
<script type="module" src="app.js"></script>
<script type="application/json">
{ "key": "value" }
</script>
Note that type="text/javascript" is valid but redundant, since JavaScript is the default. You can safely omit it for standard scripts.
The <a> element with an href attribute is one of HTML's most fundamental interactive elements. Browsers and assistive technologies inherently recognize it as a link — it's focusable via the Tab key, activatable with Enter, and announced as "link" by screen readers. This built-in behavior is part of the element's implicit ARIA role, which is link.
When you explicitly add role="link" to an <a href="..."> element, you're telling assistive technologies something they already know. The W3C validator flags this as unnecessary because it violates the principle of not redundantly setting ARIA roles that match an element's native semantics. This principle is codified in the first rule of ARIA use: "If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so."
While a redundant role="link" won't typically break anything, it creates noise in your markup. It can also signal to other developers that the role is necessary, leading to confusion or cargo-cult patterns. Clean, semantic HTML that relies on native roles is easier to maintain and less error-prone.
The role="link" attribute is legitimately useful when a non-interactive element like a <span> or <div> needs to behave as a link. In that case, you must also manually implement keyboard interaction (focus via tabindex, activation via Enter key handling) and provide an accessible name. But when you already have a proper <a> element with href, all of that comes for free — no ARIA needed.
Examples
❌ Incorrect: redundant role="link" on an anchor
<a href="/about" role="link">About Us</a>
The role="link" is redundant here because the <a> element with href already has an implicit role of link.
✅ Correct: anchor without redundant role
<a href="/about">About Us</a>
Simply remove the role="link" attribute. The browser and assistive technologies already treat this as a link.
✅ Correct: using role="link" on a non-semantic element (when necessary)
<span role="link" tabindex="0" onclick="location.href='/about'" onkeydown="if(event.key==='Enter') location.href='/about'">
About Us
</span>
This is the legitimate use case for role="link" — when you cannot use a native <a> element and need to make a non-interactive element behave like a link. Note the additional work required: tabindex="0" for keyboard focusability, a click handler, and a keydown handler for Enter key activation. Using a proper <a> element avoids all of this extra effort.
❌ Incorrect: multiple anchors with redundant roles
<nav>
<a href="/" role="link">Home</a>
<a href="/products" role="link">Products</a>
<a href="/contact" role="link">Contact</a>
</nav>
✅ Correct: clean navigation without redundant roles
<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
<a href="/contact">Contact</a>
</nav>
The role="list" attribute is redundant on an <ol> element because it already has an implicit ARIA role of list.
HTML elements come with built-in (implicit) ARIA roles that convey their purpose to assistive technologies. The <ol> and <ul> elements both have an implicit role of list, so explicitly adding role="list" is unnecessary and creates noise in your markup.
That said, there's a well-known reason some developers add this role intentionally. Safari removes list semantics when list-style: none is applied via CSS. Adding role="list" is a common workaround to restore those semantics for VoiceOver users. If this is your situation, the W3C warning is technically correct but you may choose to keep the role for accessibility reasons.
If you don't need the Safari workaround, simply remove the role attribute.
Before
<ol role="list">
<li>First item</li>
<li>Second item</li>
<li>Third item</li>
</ol>
After
<ol>
<li>First item</li>
<li>Second item</li>
<li>Third item</li>
</ol>
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