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.
When a browser encounters a document, the doctype declaration is the first thing it reads to determine which rendering mode to use. There are three modes: quirks mode, almost standards mode, and full standards mode. Almost standards mode was introduced as a transitional step — it renders pages mostly according to standards but retains a few legacy behaviors, particularly around how images inside table cells are laid out. Doctypes that trigger this mode include:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
These doctypes were common during the XHTML and HTML 4 era but are now considered obsolete. The W3C HTML Validator (Nu HTML Checker) is designed for HTML5 documents and expects the simple <!DOCTYPE html> declaration.
Why this matters
- Standards compliance: The HTML Living Standard (maintained by WHATWG) specifies
<!DOCTYPE html>as the only valid doctype. Using anything else means your document does not conform to the current standard. - Rendering consistency: While almost standards mode is close to full standards mode, the subtle differences — especially around vertical sizing of table cell images — can lead to unexpected layout behavior across browsers.
- Future-proofing: Legacy doctypes tie your pages to outdated specifications. Modern tooling, linters, and validators all expect HTML5, and maintaining an old doctype can mask other issues.
- Simplicity: The HTML5 doctype is short, easy to remember, and case-insensitive. There's no reason to use a longer, more complex declaration.
How to fix it
Replace the legacy doctype on the very first line of your HTML document with <!DOCTYPE html>. No public identifier, no system identifier, and no DTD URL is needed. After making the change, review your page for any layout shifts — in most cases there will be none, but pages that relied on the subtle almost-standards-mode behavior around images in table cells may need a small CSS adjustment (such as setting img { display: block; } or img { vertical-align: bottom; } on affected images).
Examples
❌ Legacy doctype triggering almost standards mode
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>My Page</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
❌ HTML 4.01 Transitional doctype
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
✅ Correct HTML5 doctype
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My Page</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
The HTML5 doctype is the shortest possible doctype that triggers full standards mode in all modern browsers. It works regardless of case (<!DOCTYPE html>, <!doctype html>, or <!Doctype Html> are all equivalent), though lowercase or the conventional mixed-case form shown above is recommended for consistency.
Unescaped & characters in HTML content or attribute values must be written as &.
In HTML, the & character signals the start of a character reference (like &, <, or ©). When the parser encounters a bare & that isn't followed by a valid reference, it produces a parse error. Browsers usually recover and display the & as intended, but the markup is technically invalid.
This most often appears in URLs with query string parameters (e.g., ?page=1&sort=asc) or in visible text like "Tom & Jerry". In both cases, every literal & must be escaped as &.
HTML examples
Invalid
<!-- Bare & in an href -->
<a href="search?category=books&sort=title&page=2">Results</a>
<!-- Bare & in visible text -->
<p>Fish & Chips</p>
Valid
<!-- Escaped & in an href -->
<a href="search?category=books&sort=title&page=2">Results</a>
<!-- Escaped & in visible text -->
<p>Fish & Chips</p>
The rendered output is identical in both cases. Browsers display & as & to the user, and links work correctly with & in href values. The HTML parser converts it back to a literal & before making the HTTP request.
The core problem is that aria-disabled="true" is purely an accessibility hint — it communicates a disabled state to assistive technologies like screen readers, but it has no effect on the actual behavior of the element. When an a element has an href attribute, the browser treats it as a valid hyperlink regardless of any ARIA attributes. Users can still click it, follow it via keyboard navigation, and navigate to its destination. This mismatch between the announced state ("disabled") and actual behavior ("fully functional link") creates a confusing and misleading experience, particularly for users of assistive technologies.
The W3C validator flags this combination because it violates the principle that ARIA states should accurately reflect an element's true interactive state. A link that claims to be disabled but still works undermines user trust and can cause real usability problems.
Why this matters
- Accessibility: Screen readers will announce the link as disabled, but users who activate it will be unexpectedly navigated away. This is disorienting and violates WCAG guidance on predictable behavior.
- Standards compliance: The HTML specification and ARIA in HTML requirements discourage or disallow this combination because it produces an unreliable user experience.
- Browser behavior: No browser will disable a link just because
aria-disabled="true"is present. Thehrefattribute always makes theaelement an active hyperlink.
How to fix it
You have two main approaches depending on your intent:
The link should be active: Remove
aria-disabled="true"and keep thehref. If the link works, don't mark it as disabled.The link should be disabled: Remove the
hrefattribute. Withouthref, theaelement becomes a placeholder link that is not interactive. You can then usearia-disabled="true"to communicate the disabled state,tabindex="-1"to remove it from the keyboard tab order, and CSS to style it as visually disabled. You should also add JavaScript to prevent activation if needed.
Examples
Incorrect
This triggers the validation error because aria-disabled="true" conflicts with the presence of href:
<a href="/dashboard" aria-disabled="true">Go to Dashboard</a>
Correct — Keep the link active
If the link should function normally, simply remove the aria-disabled attribute:
<a href="/dashboard">Go to Dashboard</a>
Correct — Disable the link
If the link should be non-actionable (e.g., a navigation item the user doesn't currently have access to), remove the href attribute and use ARIA and CSS to communicate the disabled state:
<a aria-disabled="true" tabindex="-1" role="link" class="link-disabled">Go to Dashboard</a>
.link-disabled {
color: #6c757d;
cursor: not-allowed;
pointer-events: none;
text-decoration: none;
}
In this approach:
- Removing
hrefensures the link is not actionable by the browser. aria-disabled="true"tells assistive technologies the element is disabled.tabindex="-1"removes the element from the keyboard tab order so users can't Tab to it.role="link"preserves the link semantics so screen readers still identify it as a link (anawithouthrefloses its implicit link role).- The CSS provides a visual indication that the element is disabled, with
pointer-events: nonepreventing mouse clicks andcursor: not-allowedgiving a visual cue on hover.
Correct — Use a button instead
If the "link" triggers an action rather than navigating somewhere, consider using a button element instead. Buttons natively support the disabled attribute:
<button type="button" disabled>Perform Action</button>
This is the simplest and most robust solution when the element doesn't need to be a link. The disabled attribute is natively understood by browsers and assistive technologies without any ARIA workarounds.
The ARIA specification defines a strict ownership hierarchy for table-related roles. A role="cell" element must be "owned by" an element with role="row", meaning it must either be a direct child of that element or be associated with it via the aria-owns attribute. This mirrors how native HTML tables work: a <td> element must live inside a <tr> element. When you use ARIA roles to build custom table structures from non-table elements like <div> or <span>, you are responsible for maintaining this same hierarchy manually.
The expected nesting order for an ARIA table is:
role="table"— the outermost containerrole="rowgroup"(optional) — groups rows together, like<thead>,<tbody>, or<tfoot>role="row"— a single row of cellsrole="cell"orrole="columnheader"/role="rowheader"— individual cells
When a role="cell" element is placed directly inside a role="table" or any other container that isn't role="row", screen readers lose the ability to announce row and column positions. Users who rely on table navigation shortcuts (such as moving between cells with arrow keys) will find the table unusable. This is not just a validation concern — it directly impacts whether people can access your content.
This issue commonly arises when developers add intermediate wrapper elements for styling purposes and accidentally break the required parent-child relationship, or when they forget to assign role="row" to a container element.
How to Fix
Ensure every element with role="cell" is a direct child of an element with role="row". If you have wrapper elements between the row and cell for layout or styling, either remove them, move the role assignments, or use aria-owns on the row element to explicitly claim ownership of the cells.
Examples
Incorrect — Cell Without a Row Parent
This triggers the validation error because role="cell" elements are direct children of the role="table" container, with no role="row" in between.
<div role="table">
<div role="cell">Name</div>
<div role="cell">Email</div>
</div>
Incorrect — Intermediate Wrapper Breaking Ownership
Here, a styling wrapper sits between the row and its cells. Since the <div> without a role is not a role="row", the cells are not properly owned.
<div role="table">
<div role="row">
<div class="cell-wrapper">
<div role="cell">Row 1, Cell 1</div>
<div role="cell">Row 1, Cell 2</div>
</div>
</div>
</div>
Correct — Cells Directly Inside Rows
Each role="cell" is a direct child of a role="row" element, forming a valid ARIA table structure.
<div role="table" aria-label="Team members">
<div role="row">
<div role="columnheader">Name</div>
<div role="columnheader">Email</div>
</div>
<div role="row">
<div role="cell">Alice</div>
<div role="cell">alice@example.com</div>
</div>
<div role="row">
<div role="cell">Bob</div>
<div role="cell">bob@example.com</div>
</div>
</div>
Correct — Using Rowgroups
You can optionally group rows with role="rowgroup", similar to <thead> and <tbody>. The cells still must be direct children of their rows.
<div role="table" aria-label="Quarterly results">
<div role="rowgroup">
<div role="row">
<div role="columnheader">Quarter</div>
<div role="columnheader">Revenue</div>
</div>
</div>
<div role="rowgroup">
<div role="row">
<div role="cell">Q1</div>
<div role="cell">$10,000</div>
</div>
<div role="row">
<div role="cell">Q2</div>
<div role="cell">$12,500</div>
</div>
</div>
</div>
Correct — Using Native HTML Instead
If your content is genuinely tabular data, consider using native HTML table elements instead of ARIA roles. Native tables have built-in semantics and require no additional role attributes.
<table>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
<tr>
<td>Alice</td>
<td>alice@example.com</td>
</tr>
</table>
Native HTML tables are always preferable when they suit your use case. The first rule of ARIA is: if you can use a native HTML element that already has the semantics you need, use it instead of adding ARIA roles to a generic element.
The ARIA specification defines a strict ownership hierarchy for table-related roles. A columnheader represents a header cell for a column, analogous to a <th> element in native HTML. For assistive technologies like screen readers to correctly announce column headers and associate them with the data cells beneath them, the columnheader must exist within a row context. The required structure is:
- An element with
role="table",role="grid", orrole="treegrid"serves as the container. - Inside it, elements with
role="rowgroup"(optional) orrole="row"organize the rows. - Each
role="row"element contains one or more elements withrole="columnheader",role="rowheader", orrole="cell".
When a role="columnheader" element is placed directly inside a role="table" or role="grid" container — or any other element that is not role="row" — the validator raises this error. Without the row wrapper, screen readers cannot navigate the table structure properly. Users who rely on assistive technology may hear disjointed content or miss the column headers entirely, making the data table unusable.
The best practice, whenever feasible, is to use native HTML table elements (<table>, <thead>, <tr>, <th>, <td>). These carry implicit ARIA roles and establish the correct ownership relationships automatically, eliminating this entire category of errors. Only use ARIA table roles when you genuinely cannot use native table markup — for example, when building a custom grid widget with non-table elements for layout reasons.
Examples
Incorrect: columnheader not inside a row
In this example, the columnheader elements are direct children of the table container, with no role="row" wrapper:
<div role="table" aria-label="Employees">
<div role="columnheader">Name</div>
<div role="columnheader">Department</div>
<div role="row">
<div role="cell">Alice</div>
<div role="cell">Engineering</div>
</div>
</div>
Correct: columnheader inside a row
Wrapping the column headers in an element with role="row" fixes the issue:
<div role="table" aria-label="Employees">
<div role="row">
<div role="columnheader">Name</div>
<div role="columnheader">Department</div>
</div>
<div role="row">
<div role="cell">Alice</div>
<div role="cell">Engineering</div>
</div>
</div>
Correct: Using rowgroup for additional structure
You can optionally use role="rowgroup" to separate headers from body rows, similar to <thead> and <tbody>. The columnheader elements must still be inside a row:
<div role="table" aria-label="Employees">
<div role="rowgroup">
<div role="row">
<div role="columnheader">Name</div>
<div role="columnheader">Department</div>
</div>
</div>
<div role="rowgroup">
<div role="row">
<div role="cell">Alice</div>
<div role="cell">Engineering</div>
</div>
</div>
</div>
Best practice: Use native table elements
Native HTML tables have built-in semantics that make ARIA roles unnecessary. A <th> inside a <tr> already behaves as a columnheader inside a row:
<table>
<thead>
<tr>
<th>Name</th>
<th>Department</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>Engineering</td>
</tr>
</tbody>
</table>
This approach is simpler, more robust across browsers and assistive technologies, and avoids the risk of ARIA misuse. Reserve ARIA table roles for situations where native table markup is not an option.
An element with role="group" cannot be nested inside an element with role="list" because the WAI-ARIA specification restricts which roles are allowed as children of a list.
The list role expects its direct children to have the listitem role. According to the ARIA specification, group is not a permitted child role of list. When a screen reader encounters a list, it announces the number of items and allows users to navigate between them. Inserting a group inside the list breaks that expected structure, which can confuse assistive technologies.
This issue commonly appears when wrapping a subset of <li> elements in a <div> with role="group" inside a <ul> or <ol>. Since <ul> and <ol> have an implicit role="list", the validator flags the violation.
If you need to visually or semantically group items within a list, you have a few options. You can use a nested list inside a <li>, which is valid HTML and produces a proper ARIA tree. Alternatively, if the grouping is purely visual, use CSS instead of a wrapper element.
Example with the issue
<ul>
<li>Apple</li>
<div role="group" aria-label="Citrus fruits">
<li>Orange</li>
<li>Lemon</li>
</div>
</ul>
This triggers the validation error because the div with role="group" is a descendant of the <ul> (which has an implicit role="list"). It also has a second problem: a <div> is not a valid direct child of <ul>.
Fixed example using a nested list
<ul>
<li>Apple</li>
<li>
Citrus fruits
<ul>
<li>Orange</li>
<li>Lemon</li>
</ul>
</li>
</ul>
The nested <ul> inside a <li> is valid HTML. Screen readers handle nested lists well, announcing the sublist and its item count separately.
Fixed example using separate lists
If a nested list does not fit your design, split the content into separate lists with headings:
<h3>All fruits</h3>
<ul>
<li>Apple</li>
</ul>
<h3>Citrus fruits</h3>
<ul>
<li>Orange</li>
<li>Lemon</li>
</ul>
This avoids nesting issues entirely and gives each group a clear label.
The WAI-ARIA specification defines strict ownership requirements for certain roles. The listitem role is one such role — it must be "owned by" an element with role="list" or role="group". "Owned by" means the listitem must be either a direct DOM child of the owning element, or explicitly associated with it via the aria-owns attribute.
This matters because screen readers and other assistive technologies rely on the accessibility tree to convey structure to users. When a screen reader encounters a properly structured list, it announces something like "list, 3 items" and lets the user navigate between items. Without the parent role="list", the individual items lose their list context — users won't know how many items exist, where the list begins and ends, or that the items are related at all.
In most cases, the simplest and most robust fix is to use native HTML list elements (<ul> or <ol> with <li> children) instead of ARIA roles. Native elements have built-in semantics that don't require additional attributes. Only use ARIA roles when native elements aren't feasible — for example, when building a custom component where the visual layout prevents using standard list markup.
Examples
Incorrect: listitem without a parent list
These listitem elements are not contained within a role="list" or role="group" parent, so the validator reports an error.
<div role="listitem">Apples</div>
<div role="listitem">Bananas</div>
<div role="listitem">Cherries</div>
Correct: wrapping items in role="list"
Adding a parent container with role="list" establishes the required ownership relationship.
<div role="list">
<div role="listitem">Apples</div>
<div role="listitem">Bananas</div>
<div role="listitem">Cherries</div>
</div>
Correct: using native HTML list elements
Native <ul> and <li> elements implicitly carry the list and listitem roles without any ARIA attributes. This is the preferred approach.
<ul>
<li>Apples</li>
<li>Bananas</li>
<li>Cherries</li>
</ul>
Correct: using role="group" for nested sublists
The role="group" container is appropriate for grouping a subset of list items within a larger list, such as a nested sublist.
<div role="list">
<div role="listitem">Fruits
<div role="group">
<div role="listitem">Apples</div>
<div role="listitem">Bananas</div>
</div>
</div>
<div role="listitem">Vegetables
<div role="group">
<div role="listitem">Carrots</div>
<div role="listitem">Peas</div>
</div>
</div>
</div>
Note that role="group" should itself be nested inside a role="list" — it doesn't replace the top-level list container, but rather serves as an intermediate grouping mechanism within one.
Correct: using aria-owns for non-descendant ownership
If the DOM structure prevents you from nesting the items directly inside the list container, you can use aria-owns to establish the relationship programmatically.
<div role="list" aria-owns="item-1 item-2 item-3"></div>
<!-- These items live elsewhere in the DOM -->
<div role="listitem" id="item-1">Apples</div>
<div role="listitem" id="item-2">Bananas</div>
<div role="listitem" id="item-3">Cherries</div>
This approach should be used sparingly, as it can create confusion if the visual order doesn't match the accessibility tree order. Whenever possible, restructure your HTML so the items are actual descendants of the list container.
The WAI-ARIA specification defines strict ownership requirements for certain roles. The menuitem role represents an option in a set of choices and is only meaningful when it exists within the context of a menu. When a menuitem appears outside of a menu or menubar, screen readers and other assistive technologies have no way to determine that it belongs to a menu widget. They cannot announce the total number of items, provide keyboard navigation between items, or convey the menu's hierarchical structure to the user.
This requirement follows the concept of required owned elements and required context roles in ARIA. Just as a <li> element belongs inside a <ul> or <ol>, a menuitem belongs inside a menu or menubar. The relationship can be established in two ways:
- DOM nesting — the
menuitemelement is a DOM descendant of themenuormenubarelement. - The
aria-ownsattribute — themenuormenubarelement usesaria-ownsto reference themenuitemby itsid, establishing ownership even when the elements aren't nested in the DOM.
It's important to note that ARIA menu roles are intended for application-style menus — the kind you'd find in a desktop application (e.g., File, Edit, View menus). They are not meant for standard website navigation. For typical site navigation, use semantic HTML elements like <nav> with <ul>, <li>, and <a> elements instead.
How to fix it
- Identify every element with
role="menuitem"in your markup. - Ensure each one is contained within an element that has
role="menu"orrole="menubar", either through DOM nesting or viaaria-owns. - Choose the correct parent role:
- Use
role="menubar"for a persistent, typically horizontal menu bar (like a desktop application's top-level menu). - Use
role="menu"for a popup or dropdown menu that contains a group of menu items.
- Use
- If you're using menus for site navigation, consider removing the ARIA menu roles entirely and using semantic HTML (
<nav>,<ul>,<li>,<a>) instead.
Examples
Incorrect — menuitem without a menu context
This triggers the validator error because the menuitem elements have no parent menu or menubar:
<div>
<div role="menuitem">Cut</div>
<div role="menuitem">Copy</div>
<div role="menuitem">Paste</div>
</div>
Correct — menuitem inside a menu
Wrapping the items in an element with role="menu" resolves the issue:
<div role="menu">
<div role="menuitem" tabindex="0">Cut</div>
<div role="menuitem" tabindex="-1">Copy</div>
<div role="menuitem" tabindex="-1">Paste</div>
</div>
Correct — menuitem inside a menubar
For a persistent horizontal menu bar with application-style actions:
<div role="menubar">
<div role="menuitem" tabindex="0">File</div>
<div role="menuitem" tabindex="-1">Edit</div>
<div role="menuitem" tabindex="-1">View</div>
</div>
Correct — nested menus with dropdown submenus
A menubar with a dropdown menu containing additional menuitem elements:
<div role="menubar">
<div role="menuitem" tabindex="0" aria-haspopup="true" aria-expanded="false">
File
<div role="menu">
<div role="menuitem" tabindex="-1">New</div>
<div role="menuitem" tabindex="-1">Open</div>
<div role="menuitem" tabindex="-1">Save</div>
</div>
</div>
<div role="menuitem" tabindex="-1" aria-haspopup="true" aria-expanded="false">
Edit
<div role="menu">
<div role="menuitem" tabindex="-1">Cut</div>
<div role="menuitem" tabindex="-1">Copy</div>
<div role="menuitem" tabindex="-1">Paste</div>
</div>
</div>
</div>
Correct — using aria-owns for ownership without DOM nesting
When the menuitem elements cannot be nested inside the menu in the DOM (e.g., due to layout constraints), use aria-owns to establish the relationship:
<div role="menu" aria-owns="item-cut item-copy item-paste"></div>
<div role="menuitem" id="item-cut" tabindex="0">Cut</div>
<div role="menuitem" id="item-copy" tabindex="-1">Copy</div>
<div role="menuitem" id="item-paste" tabindex="-1">Paste</div>
Better alternative — use semantic HTML for site navigation
If you're building standard website navigation (not an application-style menu), avoid ARIA menu roles altogether:
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
This approach is simpler, more accessible by default, and doesn't trigger the validator warning. Reserve role="menu", role="menubar", and role="menuitem" for true application-style menus that implement full keyboard interaction patterns as described in the ARIA Authoring Practices Guide.
An element with role="row" must sit inside an element whose role is table, grid, treegrid, or rowgroup, or be claimed by one through aria-owns. A row on its own has no structure to belong to, so assistive technologies cannot tell which table or grid it is part of.
The row role describes one row of a composite structure. Those four allowed owners fall into two groups: table and rowgroup for static tabular data, and grid and treegrid for interactive widgets that support keyboard navigation. In every case the container supplies the context a screen reader needs to announce the row's position and its cells. Placed outside such a container, the row is orphaned and the whole table semantics break down.
The most reliable fix is to nest the role="row" element inside its container in the DOM. When your layout prevents that, keep the row where it is and point aria-owns from the container at the row's id.
Invalid example
The role="row" is a sibling of the table, so nothing contains or owns it:
<div role="table" aria-label="Prices"></div>
<div role="row">
<span role="cell">Basic</span>
<span role="cell">$9</span>
</div>
Valid example
Nest the row inside the element with role="table":
<div role="table" aria-label="Prices">
<div role="row">
<span role="cell">Basic</span>
<span role="cell">$9</span>
</div>
</div>
If the DOM nesting is not possible, leave the row in place and add aria-owns="price-row" to the container so assistive technologies still treat it as the owner.
The WAI-ARIA specification defines a strict ownership model for tab-related roles. An element with role="tab" controls the visibility of an associated role="tabpanel" element, and tabs are expected to be grouped within a tablist. This relationship is how assistive technologies like screen readers understand and communicate the tab interface pattern to users — for example, announcing "tab 2 of 4" when focus moves between tabs.
When a tab is not contained in or owned by a tablist, screen readers cannot determine how many tabs exist in the group, which tab is currently selected, or how to navigate between them. This fundamentally breaks the accessibility of the tab interface, making it confusing or unusable for people who rely on assistive technologies.
There are two ways to establish the required relationship:
- Direct containment: Place the
role="tab"elements as direct children of therole="tablist"element. This is the most common and straightforward approach. - Using
aria-owns: If the DOM structure prevents direct nesting, addaria-ownsto thetablistelement with a space-separated list ofidvalues referencing each tab. This tells assistive technologies that thetablistowns those tabs even though they aren't direct children in the DOM.
Examples
Incorrect: tab outside of a tablist
In this example, the role="tab" buttons are siblings of the tablist rather than children of it, which triggers the validation error.
<div class="tabs">
<div role="tablist" aria-label="Sample Tabs"></div>
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
First Tab
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">
Second Tab
</button>
</div>
Correct: tabs as direct children of tablist
The simplest fix is to place the role="tab" elements directly inside the role="tablist" element.
<div class="tabs">
<div role="tablist" aria-label="Sample Tabs">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">
First Tab
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">
Second Tab
</button>
</div>
<div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
<p>Content for the first panel</p>
</div>
<div id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden>
<p>Content for the second panel</p>
</div>
</div>
Correct: using aria-owns when DOM nesting isn't possible
If your layout or framework makes it difficult to nest the tabs directly inside the tablist, you can use aria-owns to establish the relationship programmatically.
<div class="tabs">
<div role="tablist" aria-label="Sample Tabs" aria-owns="tab-1 tab-2"></div>
<div class="tab-wrapper">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">
First Tab
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">
Second Tab
</button>
</div>
<div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
<p>Content for the first panel</p>
</div>
<div id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden>
<p>Content for the second panel</p>
</div>
</div>
Additional notes
- Each
role="tab"element should usearia-selectedto indicate which tab is active ("true") and which are not ("false"). - Use
aria-controlson each tab to reference theidof its associatedtabpanel. - Use
aria-labelledbyon eachtabpanelto point back to its controlling tab. - Set
tabindex="0"on the active tab andtabindex="-1"on inactive tabs to support keyboard navigation with arrow keys within thetablist. - Always include an
aria-labeloraria-labelledbyon thetablistto give it an accessible name.
An element with role="tab" must sit inside an element with role="tablist", or be claimed by one through the aria-owns attribute. When a tab lives outside a tablist, screen readers can't tell how many tabs are in the group or which one is selected, so the whole tab interface breaks for people who rely on assistive technology. To fix it, place your role="tab" elements inside a role="tablist" container.
The tab role only carries meaning inside a tablist. A tab controls the visibility of a matching role="tabpanel", and the tablist groups the tabs together so assistive technologies can announce position (for example, "tab 2 of 4") and move focus between tabs with the arrow keys. Take the tab out of its tablist and that grouping disappears, leaving the screen reader with a button it can't place in any structure.
The W3C validator emits this message in two slightly different wordings depending on its version: an element with the “role” value “tablist” and an element with “role=tablist”. Both report the same problem, and the fix is identical.
There are two ways to establish the required relationship. Nesting the tabs directly inside the tablist is the common one. When your layout or framework makes that nesting impossible, point aria-owns from the tablist at the tab ids instead.
Invalid example
Here the role="tab" buttons are siblings of the tablist rather than children of it, so no tablist contains or owns them.
<div class="tabs">
<div role="tablist" aria-label="Account settings"></div>
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
Profile
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">
Billing
</button>
</div>
Valid example
Moving the tabs inside the role="tablist" element gives them the parent they need.
<div class="tabs">
<div role="tablist" aria-label="Account settings">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">
Profile
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">
Billing
</button>
</div>
<div id="panel-1" role="tabpanel" aria-labelledby="tab-1">
<p>Profile settings</p>
</div>
<div id="panel-2" role="tabpanel" aria-labelledby="tab-2" hidden>
<p>Billing settings</p>
</div>
</div>
If you genuinely can't nest the tabs in the DOM, keep them where they are and add aria-owns="tab-1 tab-2" to the role="tablist" element so assistive technologies still treat the list as the owner of those tabs.
An element with role="menuitem" cannot be a descendant of an <a> element because the <a> element already has an implicit interactive role, and nesting interactive or widget roles inside it creates conflicting semantics for assistive technologies.
The <a> element has an implicit ARIA role of link. When a role="menuitem" element is placed inside an <a>, screen readers receive contradictory signals: the outer element says "this is a link" while the inner element says "this is a menu item." The WAI-ARIA specification restricts which roles can appear as descendants of interactive elements to prevent this kind of ambiguity.
The fix depends on what you're trying to build. If you want a navigation menu where each item is a link, apply role="menuitem" directly on the <a> element itself, rather than on a child inside it. The <a> element can accept the menuitem role, which overrides its default link role.
HTML examples
Invalid: role="menuitem" nested inside an anchor
<ul role="menu">
<li role="none">
<a href="/home">
<span role="menuitem">Home</span>
</a>
</li>
<li role="none">
<a href="/about">
<span role="menuitem">About</span>
</a>
</li>
</ul>
Valid: role="menuitem" applied directly to the anchor
<ul role="menu">
<li role="none">
<a href="/home" role="menuitem">Home</a>
</li>
<li role="none">
<a href="/about" role="menuitem">About</a>
</li>
</ul>
Setting role="menuitem" on the <a> element itself gives assistive technologies a single, clear role. The role="none" on each <li> removes the default listitem semantics, which is expected when using the menu pattern since the menu role manages its own item structure.
The HTML specification defines strict rules about which elements can be nested inside others. The <a> element is classified as interactive content, and its content model explicitly forbids other interactive content as descendants. Elements with role="button" — whether a native <button> or any element with the ARIA role — are also interactive. Nesting one inside the other creates an ambiguous situation: should a click activate the link or the button?
This matters for several important reasons:
- Accessibility: Screen readers and assistive technologies rely on a clear, unambiguous element hierarchy. When a button is inside a link, the announced role becomes confusing — users may hear both a link and a button, or the assistive technology may only expose one of them, hiding the other's functionality.
- Unpredictable behavior: Browsers handle nested interactive elements inconsistently. Some may split the elements apart in the DOM, while others may swallow click events. This leads to broken or unreliable behavior across different browsers.
- Standards compliance: The WHATWG HTML Living Standard and W3C HTML specification both explicitly prohibit this nesting pattern.
To fix this issue, decide what the element should actually do. If it navigates to a URL, use an <a> element. If it performs an action (like submitting a form or triggering JavaScript), use a <button>. If you need both visual styles, use CSS to style one element to look like the other.
Examples
❌ Incorrect: Element with role="button" inside an <a>
<a href="/dashboard">
<span role="button">Go to Dashboard</span>
</a>
The <span> with role="button" is interactive content nested inside the interactive <a> element.
❌ Incorrect: <button> inside an <a>
<a href="/settings">
<button>Open Settings</button>
</a>
A <button> element is inherently interactive and must not appear inside an <a>.
✅ Correct: Use a link styled as a button
If the purpose is navigation, use the <a> element and style it with CSS:
<a href="/dashboard" class="btn">Go to Dashboard</a>
✅ Correct: Use a button that navigates via JavaScript
If you need a button that also navigates, handle navigation in JavaScript:
<button type="button" onclick="location.href='/dashboard'">Go to Dashboard</button>
✅ Correct: Place elements side by side
If you truly need both a link and a button, keep them as siblings rather than nesting one inside the other:
<div class="actions">
<a href="/dashboard">Go to Dashboard</a>
<button type="button">Save Preference</button>
</div>
✅ Correct: Link styled as a button using ARIA (when appropriate)
If the element navigates but you want assistive technologies to announce it as a button, you can apply role="button" directly to the element — just don't nest it inside an <a>:
<span role="button" tabindex="0" onclick="location.href='/dashboard'">
Go to Dashboard
</span>
Note that using role="button" on a non-interactive element like <span> requires you to also add tabindex="0" for keyboard focusability and handle keydown events for the Enter and Space keys. In most cases, a native <a> or <button> element is the better choice.
When an element has role="button", assistive technologies treat it as a single interactive control — just like a native <button>. Users expect to tab to it once, and then activate it with Enter or Space. If a focusable descendant (an element with tabindex) exists inside that button, it creates a second tab stop within what should be a single control. This breaks the expected interaction model and confuses both keyboard users and screen readers.
The WAI-ARIA specification explicitly states that certain roles, including button, must not contain interactive or focusable descendants. This is because a button is an atomic widget — it represents one action and should receive focus as a single unit. When a screen reader encounters a role="button" element, it announces it as a button and expects the user to interact with it directly. A nested focusable element disrupts this by creating an ambiguous focus target: should the user interact with the outer button or the inner focusable element?
This issue commonly arises when developers wrap inline elements like <span> or <a> with tabindex inside a <div role="button">, often to style parts of the button differently or to add click handlers. The correct approach is to ensure only the outermost button-like element is focusable.
How to fix it
Use a native
<button>element. This is always the best solution. Native buttons handle focus, keyboard interaction (Enter and Space key activation), and accessibility announcements automatically — noroleortabindexneeded.Move
tabindexto therole="button"container. If you must userole="button"(for example, when a<div>needs to behave as a button due to design constraints), placetabindex="0"on the container itself and removetabindexfrom all descendants.Remove
tabindexfrom descendants. If the inner element doesn't actually need to be independently focusable, simply remove thetabindexattribute from it.
When using role="button" on a non-interactive element, remember you also need to implement keyboard event handlers for Enter and Space to fully replicate native button behavior.
Examples
Incorrect: focusable descendant inside role="button"
<div role="button">
<span tabindex="0">Click me</span>
</div>
The <span> with tabindex="0" creates a focusable element inside the role="button" container, which violates the ARIA authoring rules.
Incorrect: anchor element inside role="button"
<div role="button" tabindex="0">
<a href="/action" tabindex="0">Perform action</a>
</div>
Even though the container itself is focusable, the nested <a> with tabindex is also focusable, creating two tab stops for what should be a single control.
Correct: use a native <button> element
<button>Click me</button>
A native <button> handles focus, keyboard events, and accessibility semantics out of the box with no additional attributes.
Correct: move tabindex to the role="button" container
<div role="button" tabindex="0">
<span>Click me</span>
</div>
The tabindex="0" is on the role="button" element itself, and the inner <span> is not independently focusable.
Correct: native button with styled inner content
<button>
<span class="icon">★</span>
<span class="label">Favorite</span>
</button>
You can still use inner elements for styling purposes inside a <button> — just don't add tabindex to them. The button manages focus as a single unit, and screen readers announce the combined text content.
The <a> element is an interactive content element — it's already focusable and keyboard-navigable by default. When you place an element with a tabindex attribute inside a link, you create a nested focus target. This means that keyboard users and screen readers encounter two (or more) focusable items where only one is expected. The browser may not handle this consistently, and the user experience becomes unpredictable: should pressing Enter activate the link, or the inner focusable element?
The HTML specification defines that certain interactive elements must not be nested within other interactive elements. An <a> element's content model explicitly forbids interactive content as descendants. Adding tabindex to any element makes it interactive (focusable), which violates this rule.
This matters for several reasons:
- Accessibility: Screen readers may announce nested focusable elements in confusing ways, or skip them entirely. Users relying on keyboard navigation may get trapped or confused by unexpected tab stops inside a link.
- Standards compliance: The W3C validator flags this as an error because it violates the HTML content model for anchor elements.
- Browser inconsistency: Different browsers may handle nested focusable elements differently, leading to unpredictable behavior across platforms.
To fix this issue, you have a few options:
- Remove the
tabindexattribute from the descendant element if it doesn't need to be independently focusable. - Restructure your markup so the focusable element is a sibling of the
<a>element rather than a descendant. - Rethink the design — if you need multiple interactive targets in the same area, consider using separate elements styled to appear as a single unit.
Examples
❌ Invalid: Element with tabindex inside an <a> element
<a href="/products">
<div tabindex="0">
<h2>Our Products</h2>
<p>Browse our full catalog</p>
</div>
</a>
The <div> has tabindex="0", making it focusable inside an already-focusable <a> element. This creates conflicting focus targets.
✅ Fixed: Remove tabindex from the descendant
<a href="/products">
<div>
<h2>Our Products</h2>
<p>Browse our full catalog</p>
</div>
</a>
Since the <a> element is already focusable and clickable, the inner <div> doesn't need its own tabindex. Removing it resolves the conflict.
❌ Invalid: span with tabindex inside a link
<a href="/profile">
<span tabindex="-1">User Name</span>
</a>
Even tabindex="-1" (which removes the element from the natural tab order but still makes it programmatically focusable) triggers this validation error when used inside an <a> element.
✅ Fixed: Remove tabindex from the span
<a href="/profile">
<span>User Name</span>
</a>
❌ Invalid: Button-like element nested inside a link
<a href="/dashboard">
<div tabindex="0" role="button">Settings</div>
</a>
✅ Fixed: Separate the interactive elements
<div class="card-actions">
<a href="/dashboard">Dashboard</a>
<button type="button">Settings</button>
</div>
If you truly need two distinct interactive elements, place them as siblings rather than nesting one inside the other. Use CSS to style them as a cohesive visual unit if needed.
The HTML specification defines button as an interactive content element that accepts phrasing content as its children, but explicitly forbids interactive content as descendants. When you add a tabindex attribute to an element, you make it focusable and potentially interactive, which violates this content model restriction.
This rule exists for important reasons. A button element is a single interactive control — when a user presses Tab, the entire button receives focus as one unit. If elements inside the button also have tabindex, screen readers and keyboard users encounter nested focusable items within what should be a single action target. This creates confusing, unpredictable behavior: users may tab into the button's internals without understanding the context, and assistive technologies may announce the inner elements separately, breaking the expected interaction pattern.
Browsers may also handle nested focusable elements inconsistently. Some may ignore the inner tabindex, while others may allow focus on the nested element but not properly trigger the button's click handler, leading to broken functionality.
How to fix it
The most straightforward fix is to remove the tabindex attribute from any elements inside the button. If the inner element was given tabindex="0" to make it focusable, it doesn't need it — the button itself is already focusable. If it was given tabindex="-1" to programmatically manage focus, reconsider whether that focus management is necessary within a button context.
If you genuinely need multiple interactive elements in the same area, restructure your markup so that the interactive elements are siblings rather than nested inside a button.
Examples
❌ Incorrect: span with tabindex inside a button
<button type="button">
<span tabindex="0">Click me</span>
</button>
The span has tabindex="0", making it a focusable descendant of the button. This violates the content model.
✅ Correct: Remove tabindex from the descendant
<button type="button">
<span>Click me</span>
</button>
The span no longer has tabindex, so the button behaves as a single focusable control.
❌ Incorrect: Multiple focusable elements inside a button
<button type="button">
<span tabindex="0" class="icon">★</span>
<span tabindex="-1" class="label">Favorite</span>
</button>
Both inner span elements have tabindex attributes, which is invalid regardless of the tabindex value.
✅ Correct: Style inner elements without making them focusable
<button type="button">
<span class="icon">★</span>
<span class="label">Favorite</span>
</button>
✅ Correct: Restructure if separate interactions are needed
If you need an icon and a separate action side by side, use sibling elements instead of nesting:
<span class="icon" aria-hidden="true">★</span>
<button type="button">Favorite</button>
This keeps the button as a clean, single interactive element while placing the decorative icon outside of it.
The id attribute uniquely identifies an element within a document, making it essential for fragment links, JavaScript targeting, CSS styling, and label associations. According to the HTML specification, an id value must contain at least one character and must not contain any ASCII whitespace. This means spaces, tabs, line feeds, carriage returns, and form feeds are all prohibited.
A common mistake is accidentally including a space in an id value, which effectively makes it look like multiple values — similar to how the class attribute works. However, unlike class, the id attribute does not support space-separated values. When a browser encounters an id with a space, behavior becomes unpredictable: some browsers may treat only the first word as the ID, while CSS and JavaScript selectors may fail entirely.
Why this matters
- Broken fragment links: A link like
<a href="#my section">won't correctly scroll to an element withid="my section"in all browsers. - JavaScript failures:
document.getElementById("my section")may not return the expected element, anddocument.querySelector("#my section")will throw a syntax error because spaces are not valid in CSS ID selectors without escaping. - CSS issues: A selector like
#my sectiontargets an element withid="my"that has a descendant<section>element — not what you intended. - Accessibility: Screen readers and assistive technologies rely on
idattributes for label associations (e.g.,<label for="...">). A brokenidcan break form accessibility.
Best practices for id values
While the HTML specification technically allows any non-whitespace character in an id, it's best practice to stick to ASCII letters, digits, hyphens (-), and underscores (_). Starting the value with a letter also ensures maximum compatibility with CSS selectors without needing escaping.
Examples
❌ Invalid: id contains a space
<div id="main content">
<p>Welcome to the page.</p>
</div>
The space between main and content makes this an invalid id.
✅ Fixed: Replace the space with a hyphen
<div id="main-content">
<p>Welcome to the page.</p>
</div>
✅ Fixed: Replace the space with an underscore
<div id="main_content">
<p>Welcome to the page.</p>
</div>
✅ Fixed: Use camelCase
<div id="mainContent">
<p>Welcome to the page.</p>
</div>
❌ Invalid: id with a space breaks label association
<label for="first name">First Name</label>
<input type="text" id="first name">
The for attribute won't properly associate with the input, breaking accessibility.
✅ Fixed: Remove whitespace from both for and id
<label for="first-name">First Name</label>
<input type="text" id="first-name">
❌ Invalid: Trailing or leading whitespace
Whitespace isn't always obvious. A trailing space or a copy-paste error can introduce invisible whitespace:
<section id="about ">
<h2>About Us</h2>
</section>
✅ Fixed: Trim all whitespace
<section id="about">
<h2>About Us</h2>
</section>
If you're generating id values dynamically (e.g., from a CMS or database), make sure to trim and sanitize the values by stripping whitespace and replacing spaces with hyphens or underscores before rendering them into HTML.
The alt attribute is one of the most important accessibility features in HTML. When a screen reader encounters an <img> element, it reads the alt text aloud so that visually impaired users understand the image's content and purpose. Without it, screen readers may fall back to reading the file name (e.g., "DSC underscore 0042 dot jpeg"), which is meaningless and confusing. Search engines also use alt text to understand and index image content, so including it benefits SEO as well.
The HTML specification requires the alt attribute on all <img> elements, with only narrow exceptions (such as when the image's role is explicitly overridden via certain ARIA attributes, or when the image is inside a <figure> with a <figcaption> that fully describes it—though even then, including alt is strongly recommended).
How to choose the right alt text
The value of the alt attribute depends on the image's purpose:
- Informative images — Describe the content concisely. For example, a photo of a product should describe the product.
- Functional images — Describe the action or destination, not the image itself. For example, a search icon used as a button should have
alt="Search", notalt="Magnifying glass". - Decorative images — Use an empty
altattribute (alt=""). This tells screen readers to skip the image entirely. Do not omit the attribute—use an empty string. - Complex images (charts, diagrams) — Provide a brief summary in
altand a longer description elsewhere on the page or via a link.
Examples
❌ Missing alt attribute
This triggers the W3C validation error:
<img src="photo.jpg">
A screen reader has no useful information to convey, and the validator flags this as an error.
✅ Informative image with descriptive alt
<img src="photo.jpg" alt="Person holding an orange tabby cat in a sunlit garden">
The alt text describes what the image shows, giving screen reader users meaningful context.
✅ Decorative image with empty alt
<img src="decorative-border.png" alt="">
When an image is purely decorative and adds no information, an empty alt attribute tells assistive technology to ignore it. This is valid and preferred over omitting the attribute.
✅ Functional image inside a link
<a href="/home">
<img src="logo.svg" alt="Acme Corp — Go to homepage">
</a>
Because the image is the only content inside the link, the alt text must describe the link's purpose.
✅ Image inside a <figure> with <figcaption>
<figure>
<img src="chart.png" alt="Bar chart showing quarterly revenue growth from Q1 to Q4 2024">
<figcaption>Quarterly revenue growth for fiscal year 2024.</figcaption>
</figure>
Even when a <figcaption> is present, including a descriptive alt attribute is best practice. The alt should describe the image itself, while the <figcaption> provides additional context visible to all users.
Common mistakes to avoid
- Don't start with "Image of" or "Picture of" — Screen readers already announce the element as an image. Write
alt="Golden retriever playing fetch", notalt="Image of a golden retriever playing fetch". - Don't use the file name —
alt="IMG_4392.jpg"is not helpful. - Don't duplicate surrounding text — If the text next to the image already describes it, use
alt=""to avoid redundancy. - Don't omit
altthinking it's optional — A missingaltattribute andalt=""are fundamentally different. Missing means the screen reader may announce the file path; empty means the screen reader intentionally skips the image.
An empty alt attribute on an img element is a deliberate signal that the image is purely decorative and carries no meaningful content. According to the WHATWG HTML specification, this maps the element to the "presentation" role in the accessibility tree, effectively hiding it from screen readers and other assistive technologies.
When you add a role attribute to an img with alt="", you're sending contradictory instructions: the empty alt says "this image is decorative, ignore it," while the role attribute says "this image has a specific semantic purpose." Browsers and assistive technologies cannot reliably resolve this conflict, which can lead to confusing or inconsistent behavior for users who rely on screen readers.
This rule exists to enforce clarity in how images are exposed to the accessibility tree. If an image is truly decorative, it should have alt="" and no role. If an image serves a functional or semantic purpose — such as acting as a button, link, or illustration — it should have both a descriptive alt value and, if needed, an appropriate role.
How to fix it
You have two options depending on the image's purpose:
The image is decorative: Remove the
roleattribute entirely. The emptyaltattribute already communicates that the image should be ignored by assistive technologies.The image is meaningful: Provide a descriptive
altvalue that explains the image's purpose. If a specificroleis genuinely needed, keep it alongside the non-emptyalt.
Examples
❌ Incorrect: empty alt with a role attribute
<img src="icon.png" alt="" role="img">
<img src="banner.jpg" alt="" role="presentation">
Even role="presentation" is redundant and invalid here — the empty alt already implies presentational semantics.
✅ Correct: decorative image with no role
<img src="icon.png" alt="">
If the image is decorative, simply remove the role attribute. The empty alt is sufficient.
✅ Correct: meaningful image with a descriptive alt and a role
<img src="warning-icon.png" alt="Warning" role="img">
If the image conveys information, give it a descriptive alt value. The role="img" is typically unnecessary here since img elements already have an implicit role of img when alt is non-empty, but it is at least valid.
✅ Correct: meaningful image used in a specific context
<button>
<img src="search-icon.png" alt="Search">
</button>
Here the image has a descriptive alt and doesn't need an explicit role because its purpose is conveyed through the alt text and its context within the button.
An img element with a role attribute cannot have an empty alt attribute because the role overrides the default semantics, and an empty alt signals that the image is decorative — two intentions that contradict each other.
When an img has alt="", it tells assistive technologies to skip the image entirely. The image is treated as presentational, equivalent to role="presentation" or role="none". Assigning a different role (such as button, link, or tab) tells assistive technologies the element has a specific function and should be announced. These two signals conflict: one says "ignore this," the other says "this is interactive" or "this has meaning."
To fix this, decide what the image actually does:
- If the image is decorative and should be ignored, remove the
roleattribute and keepalt="". - If the image has a functional role, provide a descriptive
altvalue that explains the image's purpose.
HTML examples
Image with conflicting role and empty alt
<img src="search.png" alt="" role="button">
Fixed: decorative image without a role
If the image is purely decorative, remove the role:
<img src="search.png" alt="">
Fixed: functional image with descriptive alt
If the image acts as a button, give it a meaningful alt:
<img src="search.png" alt="Search" role="button">
An img element without an alt attribute cannot carry any aria-* attributes except aria-hidden.
When an img lacks an alt attribute, its role and purpose are undefined from an accessibility standpoint. The HTML specification treats this as an image whose text alternative has not been provided, and assistive technologies handle it in a special fallback way. In this state, adding ARIA attributes like aria-label, aria-labelledby, or aria-describedby creates a contradiction: the markup simultaneously says "this image has no known alternative" and "here is semantic information about this image."
The only ARIA attribute allowed on an img without alt is aria-hidden="true", because hiding the image from the accessibility tree is consistent with having no text alternative.
There are two ways to fix this. Either add an alt attribute to the img (which you should almost always do), or remove the ARIA attributes and optionally add aria-hidden="true" if the image is purely decorative.
HTML examples
Invalid: img with no alt but with aria-label
<img src="photo.jpg" aria-label="A sunset over the ocean">
Fixed: add an alt attribute
If the image conveys meaning, provide an alt attribute. Once alt is present, ARIA attributes are allowed.
<img src="photo.jpg" alt="A sunset over the ocean">
Fixed: decorative image hidden from assistive technology
If the image is decorative and carries no meaning, use an empty alt or drop ARIA attributes in favor of aria-hidden:
<img src="decoration.jpg" alt="">
<img src="decoration.jpg" aria-hidden="true">
The defer attribute is invalid on an inline <script>, because it only affects scripts that are loaded from an external file through the src attribute.
defer tells the browser to download an external script in parallel with parsing and run it after the document has finished parsing. An inline script has no external file to download, so there is nothing to defer and the browser runs it as soon as it is parsed. Because the attribute has no defined effect here, the validator flags it.
This usually happens when a template or a performance plugin adds defer to every <script> on the page, including the inline ones. The same applies to async, which is likewise only valid together with src.
Invalid example
<script defer>
console.log("Hello");
</script>
Valid example
Remove the defer attribute from the inline script:
<script>
console.log("Hello");
</script>
If you need the code to run after the document is parsed, move it into an external file and defer that instead:
<script src="app.js" defer></script>
The autocomplete attribute tells the browser whether it can assist the user in filling out a form field, and if so, what type of data is expected. The generic values "on" and "off" control whether the browser should offer autofill suggestions to the user. Since type="hidden" inputs are never displayed and never receive direct user input, these values don't apply — there's no user interaction to assist with.
According to the HTML specification, hidden inputs can have an autocomplete attribute, but only with specific named autofill detail tokens (like "transaction-id" or "cc-number"). These tokens serve a programmatic purpose by providing hints about the semantic meaning of the hidden value, which can be useful for form processing. The generic "on" and "off" values, however, are explicitly disallowed because they only relate to the user-facing autofill behavior.
This validation error matters for standards compliance and can indicate a logical mistake in your markup. If you added autocomplete="off" to a hidden input hoping to prevent the browser from caching or modifying the value, it won't have that effect. Hidden input values are controlled entirely by the server or by JavaScript, not by browser autofill.
How to fix it
- Remove the
autocompleteattribute if it's not needed — this is the most common fix. - Use a specific autofill token if you need to convey semantic meaning about the hidden value (e.g.,
autocomplete="transaction-id"). - Reconsider the input type — if the field genuinely needs autofill behavior controlled, it probably shouldn't be
type="hidden".
Examples
Incorrect: using autocomplete="off" on a hidden input
<form action="/submit" method="post">
<input type="hidden" name="token" value="abc123" autocomplete="off">
<button type="submit">Submit</button>
</form>
Incorrect: using autocomplete="on" on a hidden input
<form action="/submit" method="post">
<input type="hidden" name="session-id" value="xyz789" autocomplete="on">
<button type="submit">Submit</button>
</form>
Correct: removing the autocomplete attribute
<form action="/submit" method="post">
<input type="hidden" name="token" value="abc123">
<button type="submit">Submit</button>
</form>
Correct: using a specific autofill token
If the hidden input carries a value with a well-defined autofill semantic, you can use a named token:
<form action="/checkout" method="post">
<input type="hidden" name="txn" value="TXN-001" autocomplete="transaction-id">
<button type="submit">Complete Purchase</button>
</form>
This is valid because "transaction-id" is a specific autofill detail token recognized by the specification, unlike the generic "on" or "off" values.
Hidden inputs are designed to carry data between the client and server without any user interaction or visual presence. The browser does not render them, screen readers do not announce them, and they are entirely excluded from the accessibility tree. Because aria-* attributes exist solely to convey information to assistive technologies, adding them to an element that assistive technologies cannot perceive is contradictory and meaningless.
The HTML specification explicitly prohibits aria-* attributes on input elements with type="hidden". This restriction exists because WAI-ARIA attributes — such as aria-label, aria-invalid, aria-describedby, aria-required, and all others in the aria-* family — are meant to enhance the accessible representation of interactive or visible elements. A hidden input has no such representation, so these attributes have nowhere to apply.
This issue commonly arises when:
- JavaScript frameworks or templating engines apply
aria-*attributes indiscriminately to all form inputs, regardless of type. - A developer changes an input's type from
"text"to"hidden"but forgets to remove the accessibility attributes that were relevant for the visible version. - Form libraries or validation plugins automatically inject attributes like
aria-invalidonto every input in a form.
To fix the issue, simply remove all aria-* attributes from any input element that has type="hidden". If the aria-* attribute was meaningful on a previously visible input, no replacement is needed — the hidden input doesn't participate in the user experience at all.
Examples
Incorrect: hidden input with aria-invalid
<form action="/submit" method="post">
<input type="hidden" name="referer" value="https://example.com" aria-invalid="false">
<button type="submit">Submit</button>
</form>
Correct: hidden input without aria-* attributes
<form action="/submit" method="post">
<input type="hidden" name="referer" value="https://example.com">
<button type="submit">Submit</button>
</form>
Incorrect: hidden input with multiple aria-* attributes
<form action="/save" method="post">
<input
type="hidden"
name="session_token"
value="abc123"
aria-label="Session token"
aria-required="true"
aria-describedby="token-help">
<button type="submit">Save</button>
</form>
Correct: all aria-* attributes removed
<form action="/save" method="post">
<input type="hidden" name="session_token" value="abc123">
<button type="submit">Save</button>
</form>
Correct: aria-* attributes on a visible input (where they belong)
If the input is meant to be visible and accessible, use an appropriate type value instead of "hidden":
<form action="/login" method="post">
<label for="username">Username</label>
<input
type="text"
id="username"
name="username"
aria-required="true"
aria-invalid="false"
aria-describedby="username-help">
<p id="username-help">Enter your registered email or username.</p>
<button type="submit">Log in</button>
</form>
An <li> element inside an element with role="listbox" may only carry a role value of option or group.
The listbox role turns a list into a select-like widget, so assistive technologies expect every item in it to be a selectable option, optionally organized under a group. Any other role on the items, such as menuitem, button, or tab, contradicts the structure announced by the container, and screen readers can no longer present the widget coherently. This often happens in hand-rolled dropdowns built on <ul> and <li>, where the container got role="listbox" but the items kept a role from an earlier design.
If the items really are selectable choices, give them role="option". If they are something else entirely, change the container role instead of forcing the items. And when the widget is a plain single or multiple choice, a native <select> needs no ARIA at all.
Invalid example
The items use menuitem, which is not allowed inside a listbox:
<ul role="listbox" aria-label="Sort order">
<li role="menuitem">Newest first</li>
<li role="menuitem">Oldest first</li>
</ul>
Valid example
Each item becomes an option:
<ul role="listbox" aria-label="Sort order">
<li role="option" aria-selected="true">Newest first</li>
<li role="option" aria-selected="false">Oldest first</li>
</ul>
Or use a native control and drop the ARIA roles entirely:
<label>Sort order
<select>
<option selected>Newest first</option>
<option>Oldest first</option>
</select>
</label>
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