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 <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>
<fontface="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:
<pstyle="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>
<pclass="welcome-text">Welcome to my website</p>
Nested <font> elements replaced with CSS
Old markup sometimes used multiple nested <font> tags:
<!-- Obsolete -->
<p>
<fontcolor="red"size="5">
Important:
<fontface="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>
<spanclass="alert-heading">
Important:
<spanclass="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>
<pclass="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"
<formrole="form"action="/subscribe"method="post">
<labelfor="email">Email:</label>
<inputtype="email"id="email"name="email">
<buttontype="submit">Subscribe</button>
</form>
This triggers the validator warning because role="form" duplicates the element's implicit role.
✅ Correct: no explicit role
<formaction="/subscribe"method="post">
<labelfor="email">Email:</label>
<inputtype="email"id="email"name="email">
<buttontype="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
<formaction="/subscribe"method="post"aria-label="Newsletter subscription">
<labelfor="email">Email:</label>
<inputtype="email"id="email"name="email">
<buttontype="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 -->
<navrole="navigation">...</nav>
<mainrole="main">...</main>
<headerrole="banner">...</header>
<footerrole="contentinfo">...</footer>
<buttonrole="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.
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>
<fieldsetrole="group">
<legend>Shipping Address</legend>
<labelfor="street">Street:</label>
<inputtype="text"id="street"name="street">
<labelfor="city">City:</label>
<inputtype="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>
<labelfor="street">Street:</label>
<inputtype="text"id="street"name="street">
<labelfor="city">City:</label>
<inputtype="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>
<fieldsetrole="radiogroup"aria-labelledby="color-legend">
<legendid="color-legend">Favorite Color</legend>
<label><inputtype="radio"name="color"value="red"> Red</label>
<label><inputtype="radio"name="color"value="blue"> Blue</label>
<label><inputtype="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>
<thid="cost">Price</th>
</tr>
<tr>
<tdheaders="product">Widget</td>
<tdheaders="price">$9.99</td>
</tr>
</table>
Correct: each headers value matches a th with the same id
<table>
<tr>
<thid="product">Product</th>
<thid="cost">Price</th>
</tr>
<tr>
<tdheaders="product">Widget</td>
<tdheaders="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>
<thid="region"rowspan="2">Region</th>
<thid="q1"colspan="2">Q1</th>
</tr>
<tr>
<thid="sales">Sales</th>
<thid="returns">Returns</th>
</tr>
<tr>
<tdheaders="region">North</td>
<tdheaders="q1 sales">1200</td>
<tdheaders="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
<h1role="heading"aria-level="1">Welcome to My Site</h1>
<h2role="heading">About Us</h2>
<h3role="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:
<divrole="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:
<h1role="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>
<h3class="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>
<iconsrc="favicon.png"></icon>
</head>
Valid: using a <link> element for a favicon
<head>
<title>My page</title>
<linkrel="icon"href="favicon.png"type="image/png">
</head>
Valid: displaying an icon inline with <img>
<p>
<imgsrc="star.svg"alt="Star"width="16"height="16"> Favorite
</p>
Valid: displaying an icon inline with <span> and CSS
<p>
<spanclass="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>
<imgsrc="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
<imgsrc="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:
<divrole="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"
<divrole="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:
<labelfor="pin">Enter your PIN:</label>
<inputid="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
<labelfor="amount">Amount ($):</label>
<inputid="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:
<labelfor="email">Email address:</label>
<inputid="email"type="email">
<labelfor="phone">Phone number:</label>
<inputid="phone"type="tel">
<labelfor="website">Website:</label>
<inputid="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:
<divcontenteditable="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 <spanitemprop="name">Liza</span>.</p>
</div>
Correct: itemprop inside an itemscope container
Adding itemscope (and optionally itemtype) to an ancestor element fixes the issue:
<divitemscopeitemtype="https://schema.org/Person">
<p>My name is <spanitemprop="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:
<divitemscopeitemtype="https://schema.org/Person">
<pitemprop="name">Liza</p>
<divitemprop="address"itemscopeitemtype="https://schema.org/PostalAddress">
<spanitemprop="addressLocality">Portland</span>,
<spanitemprop="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:
<divitemscopeitemtype="https://schema.org/Person"itemref="user-name"></div>
<pid="user-name">
My name is <spanitemprop="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:
<divitemscopeitemtype="https://schema.org/Product">
<spanitemprop="name">Widget</span>
</div>
<!-- This was moved out of the div above -->
<spanitemprop="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:
<divitemtype="https://schema.org/Person">
<p><spanitemprop="name">Liza Jane</span></p>
<p><spanitemprop="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:
<divitemscopeitemtype="https://schema.org/Person">
<p><spanitemprop="name">Liza Jane</span></p>
<p><spanitemprop="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:
<divitemscopeitemtype="https://schema.org/Organization">
<spanitemprop="name">Acme Corp</span>
<divitemprop="founder"itemscopeitemtype="https://schema.org/Person">
<spanitemprop="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
<inputtype="text"name="first"placeholder="First">
<inputtype="text"name="last"placeholder="Last">
</label>
This is invalid because the <label> contains two <input> descendants.
✅ Fixed: Separate labels for each input
<labelfor="first">First name</label>
<inputtype="text"id="first"name="first">
<labelfor="last">Last name</label>
<inputtype="text"id="last"name="last">
✅ Fixed: Using a fieldset to group related controls
<fieldset>
<legend>Name</legend>
<label>
First
<inputtype="text"name="first">
</label>
<label>
Last
<inputtype="text"name="last">
</label>
</fieldset>
❌ Incorrect: A select and a button inside one label
<label>
Pick your age
<selectname="age">
<option>Young</option>
<option>Old</option>
</select>
<buttontype="button">Help</button>
</label>
✅ Fixed: Button moved outside the label
<label>
Pick your age
<selectname="age">
<option>Young</option>
<option>Old</option>
</select>
</label>
<buttontype="button">Help</button>
✅ Correct: One control inside a label (implicit association)
<label>
Age
<selectid="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
<scriptlanguage="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
<scripttype="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
<scriptlanguage="JavaScript">
console.log("Hello, world!");
</script>
<scriptlanguage="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:
<scriptsrc="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:
<scripttype="module"src="app.js"></script>
<scripttype="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
<ahref="/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
<ahref="/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)
<spanrole="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>
<ahref="/"role="link">Home</a>
<ahref="/products"role="link">Products</a>
<ahref="/contact"role="link">Contact</a>
</nav>
✅ Correct: clean navigation without redundant roles
<nav>
<ahref="/">Home</a>
<ahref="/products">Products</a>
<ahref="/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
<olrole="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>
The HTML specification assigns implicit ARIA roles to many elements, meaning browsers and assistive technologies already understand their purpose without any extra attributes. The ul element has a built-in role of list, the nav element has a role of navigation, the button element has a role of button, and so on. When you explicitly add a role that matches the element's implicit role, it creates redundancy that the validator warns about.
This principle is formalized as the first rule of ARIA use: do not use ARIA if a native HTML element already provides the semantics you need. Adding redundant ARIA roles clutters your markup, can confuse developers maintaining the code, and in rare edge cases may cause assistive technologies to announce information twice or behave unexpectedly.
This same warning applies to other elements with implicit roles, such as adding role="navigation" to a nav element, role="banner" to a header element, or role="contentinfo" to a footer element.
A note about Safari and list-style: none
There is one well-known exception worth mentioning. Safari intentionally removes list semantics from ul and ol elements when list-style: none is applied via CSS. This means VoiceOver on macOS and iOS will not announce the element as a list. In this specific case, some developers deliberately add role="list" to restore the list semantics. While the W3C validator will still flag it as redundant (since it evaluates HTML in isolation, without considering CSS), this is a legitimate accessibility pattern where the redundant role serves a real purpose. If you're in this situation, you may choose to keep role="list" and accept the validator warning.
Examples
Incorrect: redundant role="list" on ul
<ulrole="list">
<li>Apples</li>
<li>Bananas</li>
<li>Cherries</li>
</ul>
Correct: relying on implicit semantics
<ul>
<li>Apples</li>
<li>Bananas</li>
<li>Cherries</li>
</ul>
Incorrect: other common redundant roles
<navrole="navigation">
<ahref="/">Home</a>
<ahref="/about">About</a>
</nav>
<mainrole="main">
<h1>Welcome</h1>
</main>
<footerrole="contentinfo">
<p>© 2024 Example Inc.</p>
</footer>
Correct: native elements without redundant roles
<nav>
<ahref="/">Home</a>
<ahref="/about">About</a>
</nav>
<main>
<h1>Welcome</h1>
</main>
<footer>
<p>© 2024 Example Inc.</p>
</footer>
Acceptable exception: restoring semantics removed by CSS
If your stylesheet strips list markers and you need to preserve list semantics for screen readers, the redundant role is a pragmatic choice:
<!-- list-style: none is applied via CSS, which removes semantics in Safari -->
<ulrole="list"class="unstyled-list">
<li>Step one</li>
<li>Step two</li>
<li>Step three</li>
</ul>
In this case, you can suppress or ignore the validator warning, understanding that it serves an accessibility need that the validator cannot detect from the HTML alone.
The listbox role is the implicit ARIA role for a <select> element only when it has a multiple attribute or a size attribute greater than 1. A standard single-selection <select> (dropdown) has an implicit role of combobox, so explicitly assigning role="listbox" to it creates a conflict.
When a <select> element has no multiple attribute and no size greater than 1, browsers render it as a collapsed dropdown — a combobox. The listbox role describes a widget where all options are persistently visible, which matches the behavior of a multi-select or a select with a visible size greater than 1. Applying role="listbox" to a standard dropdown misrepresents the control to assistive technologies.
You have a few options to fix this: remove the role="listbox" entirely (since the browser already assigns the correct implicit role), add the multiple attribute, or set size to a value greater than 1.
Incorrect Example
<selectrole="listbox"name="color">
<optionvalue="red">Red</option>
<optionvalue="blue">Blue</option>
<optionvalue="green">Green</option>
</select>
Fixed Examples
Remove the explicit role and let the browser handle it:
<selectname="color">
<optionvalue="red">Red</option>
<optionvalue="blue">Blue</option>
<optionvalue="green">Green</option>
</select>
Or, if you genuinely need role="listbox", use multiple or size greater than 1:
<selectrole="listbox"name="color"multiple>
<optionvalue="red">Red</option>
<optionvalue="blue">Blue</option>
<optionvalue="green">Green</option>
</select>
<selectrole="listbox"name="color"size="3">
<optionvalue="red">Red</option>
<optionvalue="blue">Blue</option>
<optionvalue="green">Green</option>
</select>
In most cases, simply removing role="listbox" is the best fix. The implicit ARIA roles already convey the correct semantics to assistive technologies without any extra attributes.
Many HTML elements have built-in (implicit) ARIA roles defined by the WAI-ARIA specification. The <li> element natively carries the listitem role when it is a child of a <ul>, <ol>, or <menu> element. Adding role="listitem" explicitly doesn't change behavior, but it clutters your markup and signals a misunderstanding of how semantic HTML and ARIA interact. This falls under the first rule of ARIA use: "If you can use a native HTML element with the semantics and behavior you require already built in, do so, instead of re-purposing an element and adding an ARIA role."
Redundant ARIA roles create several problems:
- Maintenance burden — Extra attributes add noise to your code, making it harder to read and maintain.
- Potential confusion — Other developers may wonder if the explicit role was added intentionally to override something, leading to uncertainty during code reviews.
- Validator warnings — Tools like the W3C HTML Validator flag these redundancies, and accumulating unnecessary warnings can obscure real issues that need attention.
The ARIA listitem role is designed for situations where you cannot use semantic HTML — for instance, when you need to create a list-like structure from generic elements like <div> or <span>. In those cases, you would pair role="list" on the container with role="listitem" on each child. But when you're already using <ul>, <ol>, or <menu> with <li> children, the ARIA roles are built in and should not be repeated.
To fix this, simply remove the role="listitem" attribute from your <li> elements. If you also have role="list" on a <ul> or <ol>, remove that too — it's equally redundant.
Examples
❌ Redundant role on <li> elements
<ulrole="list">
<lirole="listitem">Apples</li>
<lirole="listitem">Bananas</li>
<lirole="listitem">Cherries</li>
</ul>
Both role="list" on the <ul> and role="listitem" on each <li> are unnecessary because these elements already carry those roles implicitly.
✅ Clean semantic HTML without redundant roles
<ul>
<li>Apples</li>
<li>Bananas</li>
<li>Cherries</li>
</ul>
The <ul> and <li> elements provide all the accessibility semantics needed without any explicit ARIA attributes.
✅ Using ARIA roles on non-semantic elements (when necessary)
If for some reason you cannot use native list elements, ARIA roles are appropriate on generic elements:
<divrole="list">
<divrole="listitem">Apples</div>
<divrole="listitem">Bananas</div>
<divrole="listitem">Cherries</div>
</div>
This is the intended use case for role="listitem" — adding list semantics to elements that don't have them natively. However, using semantic <ul>/<ol> with <li> is always preferred when possible.
The longdesc attribute was originally designed to point to a URL containing a detailed description of an element's content, primarily as an accessibility feature for screen reader users. On <iframe> elements, it was intended to describe the framed content for users who couldn't perceive it directly. However, the attribute was poorly implemented across browsers, rarely used by assistive technologies, and was ultimately removed from the HTML specification for <iframe> elements.
Using obsolete attributes causes W3C validation errors and can create a false sense of accessibility. Since browsers and assistive technologies don't reliably process longdesc on iframes, users who need the description may never actually reach it. A visible link, on the other hand, is universally accessible — it works for all users regardless of their browser, device, or assistive technology.
The fix is straightforward: remove the longdesc attribute from the <iframe> and place a regular <a> element near the iframe that links to the description page. This approach is more robust because the link is visible, discoverable, and works everywhere.
Examples
❌ Obsolete: Using longdesc on an <iframe>
<iframesrc="report.html"title="Annual report"longdesc="report-description.html"></iframe>
This triggers the validation error because longdesc is no longer a valid attribute on <iframe>.
✅ Fixed: Using a visible link instead
<iframesrc="report.html"title="Annual report"></iframe>
<p>
<ahref="report-description.html">Read a detailed description of the annual report</a>
</p>
The longdesc attribute is removed, and a descriptive link is placed adjacent to the iframe so all users can access it.
✅ Alternative: Wrapping in a <figure> for better semantics
For a more structured approach, you can use a <figure> element with a <figcaption> to associate the description link with the iframe:
<figure>
<iframesrc="report.html"title="Annual report"></iframe>
<figcaption>
Annual report overview.
<ahref="report-description.html">Read the full description</a>.
</figcaption>
</figure>
This groups the iframe and its caption together semantically, making the relationship between them clear to both sighted users and assistive technologies.
✅ Alternative: Using aria-describedby for additional context
If you want to provide a short inline description that assistive technologies can announce automatically, you can use aria-describedby:
<pid="report-desc">
This iframe contains the interactive annual report for 2024.
<ahref="report-description.html">Read the full description</a>.
</p>
<iframesrc="report.html"title="Annual report"aria-describedby="report-desc"></iframe>
This links the iframe to the descriptive paragraph programmatically. Screen readers will announce the content of the referenced element when the iframe receives focus, while the visible link remains available to all users.
Key Takeaways
- Always include a
titleattribute on<iframe>elements to describe their purpose — this is an important baseline accessibility practice. - Replace
longdescwith a visible<a>element that links to the long description. - Place the link near the iframe so users can easily find it.
- Consider using
<figure>and<figcaption>oraria-describedbyfor stronger semantic association between the iframe and its description.
The longdesc attribute dates back to HTML4, where it accepted a URL pointing to a separate page (or section of a page) containing a detailed description of the image. The idea was to supplement the short text in the alt attribute with a more comprehensive explanation, particularly useful for complex images like charts, diagrams, or infographics.
HTML5 made longdesc obsolete for several reasons. Browser support was inconsistent — most browsers never exposed the attribute in a way that was easily discoverable by users. Many developers misused it by placing literal descriptions in the attribute instead of URLs, or left it pointing to broken links. Because the attribute was invisible in the rendered page, there was no visual indication that a longer description existed, making it practically useless for sighted users and unreliable for assistive technology users.
The recommended replacements are more robust and accessible:
- Wrap the image in an
aelement (or place a link nearby) that points to the description page. This makes the link visible and usable by everyone. - Use
aria-describedbyto reference a description that already exists on the same page. This is ideal when the detailed description is displayed alongside the image. - Use a
figurewithfigcaptionto associate a visible caption or description directly with the image.
These approaches are better for accessibility because they work reliably across browsers and assistive technologies, and they make the description discoverable to all users, not just those using specific screen readers that happened to support longdesc.
Examples
❌ Obsolete: using longdesc
<img
src="cat.jpg"
alt="Smiling cat sitting on a windowsill"
longdesc="descriptions/smiling-cat.html">
This triggers the validation error because longdesc is no longer a valid attribute on img in HTML5.
✅ Fix: wrap the image in a link
The simplest replacement is to make the image itself a link to the description:
<ahref="descriptions/smiling-cat.html">
<imgsrc="cat.jpg"alt="Smiling cat sitting on a windowsill">
</a>
✅ Fix: provide a separate link near the image
If you don't want the image itself to be clickable, place a visible link nearby:
<figure>
<imgsrc="chart.png"alt="Quarterly revenue chart for 2024">
<figcaption>
Quarterly revenue chart.
<ahref="descriptions/revenue-chart.html">View detailed description</a>
</figcaption>
</figure>
✅ Fix: use aria-describedby for on-page descriptions
When the long description is already on the same page, reference it with aria-describedby:
<figure>
<img
src="chart.png"
alt="Quarterly revenue chart for 2024"
aria-describedby="chart-description">
<figcaptionid="chart-description">
Revenue grew from $2.1M in Q1 to $3.8M in Q4, with the largest
quarter-over-quarter increase occurring between Q2 and Q3.
</figcaption>
</figure>
This approach keeps the description visible on the page and programmatically associates it with the image for screen readers.
Choosing the right approach
| Scenario | Recommended approach |
|---|---|
| Description is on a separate page | Wrap image in an a element or add a nearby link |
| Description is visible on the same page | Use aria-describedby pointing to the description's id |
| Image needs a brief visible caption | Use figure with figcaption |
| Complex image (chart, diagram, infographic) | Combine figure, figcaption, and a link to a full description |
In all cases, make sure the alt attribute still provides a meaningful short description. The long description supplements alt — it doesn't replace it.
The <main> element serves a very specific purpose in HTML: it identifies the primary content of the document's <body> — the content that is directly related to or expands upon the central topic of the page. Because of this document-level role, the HTML specification restricts where <main> can appear. It must not be a descendant of <article>, <aside>, <header>, <footer>, or <nav>. These are all sectioning or structural elements that represent subsets of the page, and nesting <main> inside them creates a semantic contradiction — you'd be saying "the main content of the whole page lives inside this one sub-section."
The <article> element, by contrast, represents a self-contained composition — something like a blog post, a news story, a forum post, or a comment. Articles are meant to be independently distributable or reusable. They logically live within the main content area of a page, not around it.
This distinction matters for accessibility. Screen readers and assistive technologies use the <main> landmark to let users skip directly to the primary content of a page. When <main> is incorrectly nested inside an <article>, assistive technologies may misinterpret the document structure, making navigation confusing or unreliable. Search engines also rely on semantic HTML to understand page structure, so incorrect nesting can affect how your content is indexed.
To fix this issue, move the <main> element out of the <article> and make it a direct child of <body> (or of a non-sectioning element like <div>). Then place your <article> elements inside <main>. Also remember that only one visible <main> element should exist per page (additional <main> elements must have the hidden attribute).
Examples
Incorrect: <main> nested inside <article>
This triggers the validation error because <main> is a descendant of <article>:
<article>
<main>
<h1>My Blog Post</h1>
<p>This is the post content.</p>
</main>
</article>
Incorrect: <main> deeply nested inside <article>
The error also triggers when <main> is an indirect descendant — it doesn't need to be a direct child:
<article>
<divclass="wrapper">
<main>
<h1>My Blog Post</h1>
<p>This is the post content.</p>
</main>
</div>
</article>
Correct: <article> inside <main>
Invert the relationship so that <main> wraps the article content:
<main>
<article>
<h1>My Blog Post</h1>
<p>This is the post content.</p>
</article>
</main>
Correct: Multiple articles inside <main>
A typical page layout with <main> containing several articles alongside other content:
<main>
<h1>Latest Posts</h1>
<article>
<h2>First Post</h2>
<p>Content of the first post.</p>
</article>
<article>
<h2>Second Post</h2>
<p>Content of the second post.</p>
</article>
</main>
Correct: Full document structure
A complete valid document showing the proper placement of <main> as a direct child of <body>:
<!DOCTYPE html>
<htmllang="en">
<head>
<title>Blog - Latest Posts</title>
</head>
<body>
<header>
<nav>
<ahref="/">Home</a>
<ahref="/about">About</a>
</nav>
</header>
<main>
<h1>Latest Posts</h1>
<article>
<h2>My Blog Post</h2>
<p>This is the post content.</p>
</article>
</main>
<footer>
<p>© 2024 My Blog</p>
</footer>
</body>
</html>
The key rule to remember: <main> represents the page's primary content and sits at the top of your content hierarchy. Sectioning elements like <article>, <aside>, and <nav> are components within that hierarchy and belong inside or alongside <main> — never around it.
The <main> element identifies the primary content of a page — the content that is directly related to or expands upon the central topic of the document. According to the WHATWG HTML living standard, a <main> element must not appear as a descendant of another <main> element. This rule exists because the semantic purpose of <main> is to mark a single, unique content region; nesting it creates a contradictory structure where one "primary content area" exists inside another.
This matters for several important reasons:
- Accessibility: Screen readers and other assistive technologies use the
<main>landmark to allow users to skip directly to the primary content. When multiple or nested<main>elements exist, this navigation breaks down — assistive technology may only recognize one of them, or it may present a confusing hierarchy of "main" landmarks to the user. - Standards compliance: Browsers and validators enforce the HTML specification's content model for
<main>. A nested<main>violates that content model and produces a validation error. - Semantic clarity: The
<main>element carries specific meaning. Nesting it dilutes that meaning and signals a structural misunderstanding of the document to both machines and other developers.
This issue commonly arises when composing pages from multiple templates or components — for example, when a layout template already wraps content in <main> and an inner component or partial also includes its own <main> element. It can also happen during refactoring when code is moved between files without checking the surrounding structure.
To fix the issue, identify the nested <main> element and replace it with a more appropriate element. If the inner content represents a thematic grouping, use <section>. If it represents a self-contained composition (like a blog post or comment), use <article>. If no particular semantic meaning is needed, a plain <div> works fine.
Examples
❌ Invalid: nested <main> elements
<main>
<h1>Welcome</h1>
<main>
<p>This nested main element is invalid.</p>
</main>
</main>
The inner <main> is a descendant of the outer <main>, which violates the content model.
✅ Fixed: inner <main> replaced with <section>
<main>
<h1>Welcome</h1>
<section>
<h2>Introduction</h2>
<p>This section is valid inside main.</p>
</section>
</main>
❌ Invalid: deeply nested <main> inside other elements
The nesting doesn't have to be direct. A <main> anywhere inside another <main> triggers this error:
<main>
<h1>Dashboard</h1>
<divclass="content-wrapper">
<article>
<main>
<p>Still invalid, even though it's nested several levels deep.</p>
</main>
</article>
</div>
</main>
✅ Fixed: replaced with <div>
<main>
<h1>Dashboard</h1>
<divclass="content-wrapper">
<article>
<div>
<p>Now valid with a neutral container element.</p>
</div>
</article>
</div>
</main>
❌ Invalid: component templates each providing <main>
This pattern often appears in frameworks where a layout and a page component both define <main>:
<!-- Layout template wraps page content -->
<main>
<!-- Page component output -->
<main>
<h1>About Us</h1>
<p>Our story begins...</p>
</main>
</main>
✅ Fixed: <main> only in the layout
<!-- Layout template wraps page content -->
<main>
<!-- Page component output -->
<h1>About Us</h1>
<p>Our story begins...</p>
</main>
Keep <main> at whichever level makes the most sense for your architecture — typically the outermost layout — and remove it from inner components. If you need to group the inner content, use <section>, <article>, or <div> instead.
The <main> element serves a specific structural role: it identifies the primary content of the page, distinct from repeated elements like headers, footers, and navigation. Because of this unique purpose, the HTML specification strictly limits where <main> can appear in the document tree. Nesting <main> inside a <section> element violates these rules because <section> represents a thematic grouping of content — placing <main> inside it implies that the dominant page content is merely a subsection of something else, which is semantically contradictory.
According to the WHATWG HTML living standard, a hierarchically correct <main> element is one whose ancestor elements are limited to <html>, <body>, <div>, <form> (without an accessible name), and autonomous custom elements. This means <main> cannot be a descendant of <article>, <aside>, <footer>, <header>, <nav>, or <section>.
Why this matters
- Accessibility: Screen readers and assistive technologies use the
<main>element as a landmark to let users skip directly to the primary content. When<main>is incorrectly nested inside<section>, assistive technologies may misinterpret the document structure, making navigation harder for users who rely on landmarks. - Standards compliance: Browsers are lenient and will render the page regardless, but the semantic meaning is broken. Future browser features or tools that depend on correct document structure may not work as expected.
- Document structure clarity: The
<main>element should clearly sit at the top level of your content hierarchy, making it immediately obvious to both developers and machines which part of the page is the primary content.
Additional rules for <main>
Beyond the ancestor restriction, remember that a document must not have more than one visible <main> element. If you use multiple <main> elements (for example, in a single-page application), all but one must have the hidden attribute specified.
Examples
Incorrect: <main> nested inside <section>
This structure places <main> as a descendant of <section>, which triggers the validation error:
<body>
<header>
<h1>My Website</h1>
</header>
<section>
<main>
<h2>Welcome</h2>
<p>This is the primary content of the page.</p>
</main>
</section>
</body>
Correct: <main> as a sibling of <section>
Move <main> out of the <section> so it is a direct child of <body>:
<body>
<header>
<h1>My Website</h1>
</header>
<main>
<h2>Welcome</h2>
<p>This is the primary content of the page.</p>
</main>
<section>
<h2>Related Topics</h2>
<p>Additional thematic content goes here.</p>
</section>
</body>
Correct: <section> elements inside <main>
If your primary content is divided into thematic sections, nest the <section> elements inside <main> — not the other way around:
<body>
<header>
<h1>My Website</h1>
</header>
<main>
<section>
<h2>Introduction</h2>
<p>An overview of the topic.</p>
</section>
<section>
<h2>Details</h2>
<p>A deeper dive into the subject.</p>
</section>
</main>
<footer>
<p>© 2024 My Website</p>
</footer>
</body>
Correct: <main> wrapped in a <div>
If your layout requires a wrapper element around <main>, a <div> is a valid ancestor:
<body>
<divclass="layout-wrapper">
<header>
<h1>My Website</h1>
</header>
<main>
<h2>Welcome</h2>
<p>This is the primary content of the page.</p>
</main>
</div>
</body>
The key principle is simple: <main> defines the dominant content of the entire document, so it belongs at the top level of your content hierarchy. Sectioning elements like <section>, <article>, <aside>, <nav>, <header>, and <footer> should never wrap <main> — instead, they should be placed as children or siblings of it.
The ARIA specification defines a set of roles that convey the purpose of an element to assistive technologies like screen readers. Many HTML elements have implicit ARIA roles — built-in semantics that map directly to ARIA roles without any extra markup. The <main> element is one of these: it automatically communicates the main landmark role to assistive technologies.
When you write <main role="main">, you're explicitly stating something the browser and assistive technologies already know. The W3C validator warns about this redundancy because it can signal a misunderstanding of how native HTML semantics work. While it won't break anything, unnecessary attributes add noise to your markup and can make code harder to maintain.
This principle applies broadly across HTML. For example, <nav> implicitly has role="navigation", <header> implicitly has role="banner" (when not nested inside a sectioning element), and <button> implicitly has role="button". Explicitly restating these roles is discouraged by both the W3C and the ARIA in HTML specification, which states: "Setting an ARIA role and/or aria-* attribute that matches the implicit ARIA semantics is unnecessary and is NOT RECOMMENDED."
Why this matters
- Code clarity: Redundant attributes make your HTML harder to read and can confuse other developers into thinking the attribute is necessary.
- Standards compliance: The W3C validator raises a warning, which can obscure more important issues in your validation reports.
- Best practices: Following the principle of using native HTML semantics without redundant ARIA keeps your code clean and aligns with the first rule of ARIA: "If you can use a native HTML element with the semantics and behavior you require already built in, do so, instead of re-purposing an element and adding an ARIA role."
How to fix it
Remove the role="main" attribute from any <main> element. The semantic meaning is already provided by the element itself.
If you're working with a <div> or another generic element that needs the main landmark role (for example, in a legacy codebase that cannot use <main>), then role="main" is appropriate and necessary on that element.
Examples
❌ Redundant role on <main>
<mainrole="main">
<h1>Welcome to my site</h1>
<p>This is the primary content of the page.</p>
</main>
The role="main" attribute is unnecessary here because <main> already implies it.
✅ Using <main> without a redundant role
<main>
<h1>Welcome to my site</h1>
<p>This is the primary content of the page.</p>
</main>
✅ Using role="main" on a non-semantic element (when necessary)
<divrole="main">
<h1>Welcome to my site</h1>
<p>This is the primary content of the page.</p>
</div>
This approach is valid when you cannot use the <main> element — for instance, due to framework constraints or legacy browser support requirements. In most modern projects, prefer the <main> element instead.
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