HTML Guides for role
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 <main> element represents the dominant, unique content of a document — the primary content that is directly related to or expands upon the central topic of the page. Having more than one visible <main> element creates ambiguity: browsers, screen readers, and other assistive technologies use <main> to identify the primary content area, and multiple visible instances make it unclear which content block is truly the main one.
This is particularly important for accessibility. Screen reader users often rely on landmark navigation to jump directly to the main content of a page. When multiple visible <main> elements exist, this shortcut becomes unreliable or confusing, as the user has no way to know which <main> holds the content they're looking for.
There are legitimate scenarios where multiple <main> elements make sense — for example, in single-page applications (SPAs) where different views are swapped in and out dynamically using JavaScript. The HTML specification accommodates this by allowing multiple <main> elements as long as only one is visible at a time. The others must be hidden using the hidden attribute.
How to fix it
- If you only need one main content area, remove the extra
<main>elements and keep just one. - If you need multiple views (e.g., for tabbed content or SPA-style navigation), add the
hiddenattribute to all<main>elements except the one currently active. Use JavaScript to toggle visibility by adding or removing thehiddenattribute as needed.
Examples
❌ Invalid: Two visible <main> elements
<header>
<h1>My Website</h1>
</header>
<main>
<h2>Welcome</h2>
<p>This is the home page content.</p>
</main>
<main>
<h2>About</h2>
<p>This is the about page content.</p>
</main>
Both <main> elements are visible, which violates the specification and confuses assistive technologies.
✅ Fixed: Single <main> element
If you don't need multiple views, simply use one <main>:
<header>
<h1>My Website</h1>
</header>
<main>
<h2>Welcome</h2>
<p>This is the home page content.</p>
</main>
✅ Fixed: Multiple <main> elements with only one visible
If you need multiple views for JavaScript-driven navigation, hide all but the active one using the hidden attribute:
<header>
<nav>
<button onclick="switchView('home')">Home</button>
<button onclick="switchView('about')">About</button>
</nav>
</header>
<main id="home">
<h2>Welcome</h2>
<p>This is the home page content.</p>
</main>
<main id="about" hidden>
<h2>About</h2>
<p>This is the about page content.</p>
</main>
In this pattern, JavaScript would toggle the hidden attribute when the user navigates between views, ensuring only one <main> is ever visible at a time.
❌ Invalid: Using CSS instead of hidden
Note that hiding a <main> element with CSS (e.g., display: none or visibility: hidden) does not satisfy the HTML specification. The validator checks for the hidden attribute, not CSS styles:
<!-- This still triggers the validation error -->
<main>
<h2>Welcome</h2>
</main>
<main style="display: none;">
<h2>About</h2>
</main>
Always use the hidden attribute to indicate that a <main> element is not currently relevant to the page.
A page must contain no more than one visible element with role="main" because this landmark role identifies the primary content area of the document, and having multiple instances creates ambiguity for assistive technologies.
Screen readers and other assistive tools use the main landmark to let users skip directly to the page's primary content. When two or more visible elements carry role="main", the tool cannot determine which one is the actual primary content block. The HTML <main> element has an implicit role="main", so mixing <main> elements with role="main" on other elements can also trigger this error.
A common cause is having multiple <main> elements without hiding the inactive ones. Single page applications sometimes keep several <main> elements in the DOM for different views. That is valid only if all but one are hidden using the hidden attribute.
Another cause is placing role="main" on a <div> while also having a <main> element elsewhere in the page.
Invalid example
<body>
<main>
<h1>Home</h1>
<p>Welcome to the home page.</p>
</main>
<main>
<h1>About</h1>
<p>About this site.</p>
</main>
</body>
Valid example
Only one <main> is visible at a time. The others use the hidden attribute:
<body>
<main>
<h1>Home</h1>
<p>Welcome to the home page.</p>
</main>
<main hidden>
<h1>About</h1>
<p>About this site.</p>
</main>
</body>
If you do not need multiple views, remove the extra <main> or role="main" entirely and keep a single one:
<body>
<header>
<h1>My Site</h1>
</header>
<main>
<p>Primary content goes here.</p>
</main>
<footer>
<p>Footer content.</p>
</footer>
</body>
The HTML <figure> element represents self-contained content — such as an image, diagram, code snippet, or quotation — optionally accompanied by a caption provided by a <figcaption> element. When a <figcaption> is present, the browser and assistive technologies already understand the relationship between the figure and its caption. This built-in semantic relationship is part of the ARIA in HTML specification, which governs how native HTML elements map to accessibility roles.
The role attribute is typically used to assign or override the ARIA role of an element for assistive technologies like screen readers. However, the ARIA in HTML specification explicitly restricts certain role assignments when an element's native semantics are already well-defined by its content. A <figure> with a <figcaption> is one such case — the presence of the caption establishes a clear semantic structure that the role attribute would interfere with.
This restriction exists for several important reasons:
- Accessibility conflicts: Adding
role="figure"is redundant since the element already has that implicit role. Adding a different role (likerole="img"orrole="group") could confuse assistive technologies by contradicting the semantic meaning established by the<figcaption>. Screen readers may ignore the caption or announce the element incorrectly. - Standards compliance: The ARIA in HTML specification states that when a
<figure>has a<figcaption>descendant, noroleattribute is allowed. The W3C validator enforces this rule. - Maintainability: Relying on native HTML semantics rather than ARIA overrides keeps your markup simpler and less error-prone. The first rule of ARIA is: "If you can use a native HTML element with the semantics and behavior you require already built in, do so."
To fix this issue, remove the role attribute from the <figure> element. The <figcaption> provides all the semantic context needed.
Examples
Incorrect: role attribute on <figure> with <figcaption>
Adding role="figure" is redundant and triggers the validation error:
<figure role="figure">
<img src="chart.png" alt="Sales data for Q3 2024">
<figcaption>Figure 1: Quarterly sales by region.</figcaption>
</figure>
Using a different role like role="img" also triggers the error and can cause accessibility problems:
<figure role="img">
<img src="chart.png" alt="Sales data for Q3 2024">
<figcaption>Figure 1: Quarterly sales by region.</figcaption>
</figure>
Correct: <figure> with <figcaption> and no role
Simply remove the role attribute. The <figure> and <figcaption> elements handle semantics on their own:
<figure>
<img src="chart.png" alt="Sales data for Q3 2024">
<figcaption>Figure 1: Quarterly sales by region.</figcaption>
</figure>
Correct: <figure> without <figcaption> can use a role
If you have a <figure> without a <figcaption>, the restriction does not apply. In this case, you may use a role attribute if needed:
<figure role="img" aria-label="Sales data for Q3 2024">
<img src="chart.png" alt="">
</figure>
Correct: <figure> with <figcaption> containing other media
The fix applies regardless of the type of content inside the <figure>:
<figure>
<pre><code>const greeting = "Hello, world!";</code></pre>
<figcaption>A simple variable assignment in JavaScript.</figcaption>
</figure>
<figure>
<blockquote>
<p>The best way to predict the future is to invent it.</p>
</blockquote>
<figcaption>Alan Kay</figcaption>
</figure>
In every case, the <figcaption> provides the accessible name and descriptive context for the <figure>, making any role attribute unnecessary and non-conformant.
An element with role="cell" has no ancestor with role="row", and no row claims it through aria-owns.
An ARIA table nests its roles in a fixed order: table, then an optional rowgroup, then row, then cell (or columnheader and rowheader). Screen readers use that structure to announce a cell's row and column and to move between cells. A cell outside any row has no position in the table, so it may be read as loose text or skipped.
The row doesn't have to be the cell's direct parent. Plain <div> or <span> wrappers between a role="row" element and its cells pass, so layout wrappers can stay. The error appears when no ancestor has the row role at all: cells placed straight inside role="table" or role="rowgroup", a row wrapper that only has a class, or a role="cell" used outside any table. When the cells can't sit inside the row in the DOM, give each one an id and list those ids in aria-owns on the row.
Newer versions of the validator word this message as an element with the “role” value “row”, with the same cause and fix.
For tabular data, a native <table> with <tr> and <td> gives you this structure without any role attributes.
Invalid example
The second row's wrapper has a class but no role="row":
<div role="table" aria-label="Team members">
<div role="row">
<div role="columnheader">Name</div>
<div role="columnheader">Role</div>
</div>
<div class="member">
<div role="cell">Alice</div>
<div role="cell">Designer</div>
</div>
</div>
Valid example
With role="row" on the wrapper, the cells belong to a row, even with an extra layout <div> in between:
<div role="table" aria-label="Team members">
<div role="row">
<div role="columnheader">Name</div>
<div role="columnheader">Role</div>
</div>
<div role="row">
<div class="row-inner">
<div role="cell">Alice</div>
<div role="cell">Designer</div>
</div>
</div>
</div>
Every element with role="cell" needs an ancestor with role="row", or a row that claims it through aria-owns.
An ARIA table nests its roles in a fixed order: table, then an optional rowgroup, then row, then cell (or columnheader and rowheader). Screen readers use that structure to announce a cell's row and column and to let people jump from cell to cell. A cell with no row above it has no position in the table, so a screen reader may read it as loose text or skip it during table navigation.
This usually happens when a table built from <div> elements has a wrapper for each row that carries a class for styling but never gets role="row". If that wrapper sits inside a role="rowgroup", the validator also reports that a child of the rowgroup must have role="row". Adding the missing role to the wrapper fixes both messages.
The row doesn't have to be the cell's direct parent. The validator accepts plain <div> or <span> wrappers between a role="row" element and its cells, so you can keep layout wrappers as long as some ancestor has the row role. When the cells can't sit inside the row in the DOM, give each cell an id and list those ids in aria-owns on the row.
Older validator versions word this message as an element with “role=row” instead of an element with the “role” value “row”, with the same cause and fix.
If the data is really tabular, a native <table> with <tr> and <td> gives you this structure without any role attributes.
Invalid example
Each plan wrapper groups two cells, but nothing marks it as a row.
<div role="table" aria-label="Subscription plans">
<div role="rowgroup">
<div class="plan">
<div role="cell">Basic</div>
<div role="cell">$10 per month</div>
</div>
<div class="plan">
<div role="cell">Pro</div>
<div role="cell">$25 per month</div>
</div>
</div>
</div>
Valid example
With role="row" on each wrapper, every cell has a row to belong to.
<div role="table" aria-label="Subscription plans">
<div role="rowgroup">
<div role="row" class="plan">
<div role="cell">Basic</div>
<div role="cell">$10 per month</div>
</div>
<div role="row" class="plan">
<div role="cell">Pro</div>
<div role="cell">$25 per month</div>
</div>
</div>
</div>
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.
The list role allows only listitem children, so an element with role="group" can't sit directly inside an element with role="list".
Screen readers announce a list with the number of items it holds and let users jump from one item to the next. A group in that position isn't a list item, so what the list announces no longer matches what's inside it. <ul> and <ol> have an implicit list role, so the same rule applies to them.
The validator checks children in the accessibility tree, not in the DOM. It looks past wrappers that have no role, so a plain <div> between the list and the group doesn't help. An element with role="listitem" between them does.
Carousels are the usual source. Some slider scripts give the slide track role="list" and each slide role="group" with aria-roledescription="slide", mixing two patterns. The carousel pattern in the ARIA Authoring Practices Guide uses group slides inside a container that isn't a list.
Invalid example
<section aria-roledescription="carousel" aria-label="Featured stories">
<div class="carousel-track" role="list">
<div role="group" aria-roledescription="slide" aria-label="1 of 2">
<img src="spring.jpg" alt="Spring collection">
</div>
<div role="group" aria-roledescription="slide" aria-label="2 of 2">
<img src="summer.jpg" alt="Summer collection">
</div>
</div>
</section>
Valid example
Remove role="list" from the track and keep the slides as groups:
<section aria-roledescription="carousel" aria-label="Featured stories">
<div class="carousel-track">
<div role="group" aria-roledescription="slide" aria-label="1 of 2">
<img src="spring.jpg" alt="Spring collection">
</div>
<div role="group" aria-roledescription="slide" aria-label="2 of 2">
<img src="summer.jpg" alt="Summer collection">
</div>
</div>
</section>
If the slides should be announced as a list, make them list items instead of groups:
<section aria-roledescription="carousel" aria-label="Featured stories">
<ul class="carousel-track">
<li aria-roledescription="slide" aria-label="1 of 2">
<img src="spring.jpg" alt="Spring collection">
</li>
<li aria-roledescription="slide" aria-label="2 of 2">
<img src="summer.jpg" alt="Summer collection">
</li>
</ul>
</section>
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 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.
An element with role="rowgroup" needs an ancestor with role="table", role="grid", or role="treegrid", or one of those elements has to claim it through aria-owns.
A rowgroup groups the rows of a table, the way <thead>, <tbody> and <tfoot> do in a native <table>. It only means something inside the structure it divides. With no table, grid or treegrid above it, a screen reader has no table to announce, so it can't report the number of rows and columns or let people move from cell to cell with its table commands.
This often shows up in cards or comparison layouts built from <div> elements. The rows and cells inside carry ARIA roles, but the outer container has only a class for styling. Add role="table" to that container and give it a name with aria-label or aria-labelledby, so screen readers can say which table it is. Pick grid or treegrid only when the widget lets people move between cells with the arrow keys.
The table doesn't have to be the rowgroup's direct parent. The validator accepts plain wrapper elements in between, as long as some ancestor has one of the three roles. When the rowgroup can't sit inside the table in the DOM, give it an id and list that id in aria-owns on the table.
If the content isn't really tabular, such as a short list of features in a card, remove role="rowgroup", role="row" and role="cell" together and use a list or plain text. If you remove only the rowgroup role, the validator reports each row instead. For real tabular data, a native <table> with <tbody>, <tr> and <td> gives you the same structure without any ARIA.
Invalid example
The rows sit in a rowgroup, but the wrapper around it has no table role.
<h2 id="specs-title">Technical details</h2>
<div class="specs">
<div role="rowgroup">
<div role="row">
<span role="cell">Weight</span>
<span role="cell">1.2 kg</span>
</div>
<div role="row">
<span role="cell">Battery life</span>
<span role="cell">18 hours</span>
</div>
</div>
</div>
Valid example
With role="table" and a name on the wrapper, the rowgroup has a table to belong to.
<h2 id="specs-title">Technical details</h2>
<div class="specs" role="table" aria-labelledby="specs-title">
<div role="rowgroup">
<div role="row">
<span role="cell">Weight</span>
<span role="cell">1.2 kg</span>
</div>
<div role="row">
<span role="cell">Battery life</span>
<span role="cell">18 hours</span>
</div>
</div>
</div>
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.
An element with an interactive ARIA role, such as menuitem or button, is nested inside another element whose role already describes a single control, like a menu item inside a menu item.
ARIA in HTML treats an element with one of these outer roles like a native <button>: it can hold phrasing content, but no interactive descendants. Most of them also have presentational children in WAI-ARIA, so browsers don't expose their descendants to assistive technologies as separate elements. A screen reader announces the outer menu item and reads anything nested inside it as plain text. The arrow-key navigation of a menu can't reach a nested item either.
The validator checks descendants at any depth inside elements with role set to button, checkbox, img, link, math, menuitem, menuitemcheckbox, menuitemradio, option, progressbar, radio, separator, slider, switch or tab. It flags descendants whose role is button, checkbox, combobox, grid, gridcell, listbox, menu, menubar, menuitem, menuitemcheckbox, menuitemradio, option, radio, scrollbar, searchbox, slider, spinbutton, switch, tab, textbox or treeitem.
Nested submenus are the most common cause. A site menu gives each <li> role="menuitem" and puts the submenu, a <ul role="menu"> with its own menu items, inside that <li>. Each submenu then gets one warning for the role="menu" list and one for every item in it. If the <li> elements also hold links, those links get their own warnings, starting with "The element "a" should not appear as a descendant of an element with the attribute "role=menuitem"." The same fix clears all of them.
In the WAI-ARIA menu pattern, a submenu sits next to the item that opens it, not inside it. Give each <li> role="none", put role="menuitem" on the link or button inside it, and place the submenu <ul role="menu"> after that element in the same <li>. Add aria-haspopup="true" and aria-expanded to the parent item so screen readers know it opens a submenu.
If the list is ordinary site navigation, remove role="menubar", role="menu" and role="menuitem" altogether. The menu roles are meant for application menus like the ones in desktop software, and they need scripts that move focus with the arrow keys. A <nav> with nested lists of links and a <button aria-expanded> for each submenu needs no ARIA roles.
Other role combinations have the same fix: keep one interactive role per control. Move the role onto the element that performs the action, or move the inner control out of the outer one.
Invalid example
<ul role="menubar" aria-label="Main">
<li role="menuitem" aria-haspopup="true" aria-expanded="false">
Services
<ul role="menu" aria-label="Services">
<li role="menuitem">Consulting</li>
<li role="menuitem">Training</li>
</ul>
</li>
</ul>
Valid example
<ul role="menubar" aria-label="Main">
<li role="none">
<button type="button" role="menuitem" aria-haspopup="true" aria-expanded="false">Services</button>
<ul role="menu" aria-label="Services">
<li role="menuitem">Consulting</li>
<li role="menuitem">Training</li>
</ul>
</li>
</ul>
Valid example for site navigation
<nav aria-label="Main">
<ul>
<li>
<button type="button" aria-expanded="false" aria-controls="services-menu">Services</button>
<ul id="services-menu" hidden>
<li><a href="/consulting">Consulting</a></li>
<li><a href="/training">Training</a></li>
</ul>
</li>
</ul>
</nav>
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.
An element with a tabindex attribute is nested inside an element whose ARIA role, such as button, tab or checkbox, describes a single control that should take focus as one unit.
ARIA in HTML gives an element with one of these roles the same content rules as the native control it imitates. A <button> can hold text and other phrasing content, but no interactive elements and no elements with a tabindex attribute. A <div role="button"> follows the same rule.
The value of tabindex doesn't matter. With tabindex="0", the inner element becomes a second tab stop inside the control. With tabindex="-1", it stays out of the tab order, but a script or a mouse click can still focus it, so focus lands on part of the control instead of the control itself. Keyboard and screen reader users expect to reach the control once and activate it with Enter or Space. Most of these roles also have presentational children in WAI-ARIA, which means browsers shouldn't expose their descendants to assistive technologies as separate elements.
The validator checks descendants at any depth inside elements with role set to button, checkbox, img, link, math, menuitem, menuitemcheckbox, menuitemradio, option, progressbar, radio, separator, slider, switch or tab. The element with the role can have tabindex itself; the validator only flags its descendants.
It's a warning because the ARIA in HTML rule behind it is non-normative. Older validator builds reported it as an error, with "must not appear" in place of "should not appear". Inside a native <button> or <a>, a descendant with tabindex is still an error.
Remove tabindex from the descendant, and keep tabindex="0" on the element with the role. A native <button> is simpler, because it's focusable and responds to Enter and Space without extra attributes or scripts. Some carousel scripts give each slide role="button" and an inner tabindex="-1" element as a focus target. A slide with its own content, like an image and a caption, isn't a button, so remove role="button" from the slide and put a real <button> inside it for the action.
Invalid example
<div role="button" tabindex="0">
<span class="label" tabindex="-1">Add to cart</span>
</div>
Valid example
<button type="button">
<span class="label">Add to cart</span>
</button>
Valid example keeping the role
<div role="button" tabindex="0">
<span class="label">Add to cart</span>
</div>
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 needs an accessible name, such as a non-empty alt attribute, an aria-label or an aria-labelledby.
The validator reports this error for any img that has a role but no name. The role value doesn't matter: role="button", role="img", role="presentation" and role="none" all trigger it. A title attribute doesn't count as a name for this check, and aria-hidden="true" doesn't make the error go away.
A common cause is a decorative image marked up as <img role="presentation"> or <img role="none">, often with aria-hidden="true" too. Those attributes try to hide the image from assistive technologies, and HTML already has a simpler way to do that: an empty alt attribute. Remove the role and aria-hidden and write alt="". Don't keep the role next to the empty alt, because the validator rejects that combination with a different error.
An image that does something, such as acting as a button, needs a name that says what it does. Adding alt="Search" to <img role="button"> is enough to pass validation, but a real button element around the image is the better fix. It gets keyboard focus and activation without extra script, and the image's alt becomes the button's name.
Invalid example
<img src="divider.png" role="presentation" aria-hidden="true">
<img src="search.png" role="button" tabindex="0">
Valid example
<img src="divider.png" alt="">
<button type="button">
<img src="search.png" alt="Search">
</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 <li> inside an element with role="menu" or role="menubar" has a role that doesn't belong in a menu.
These two roles describe an application menu, like the one at the top of a desktop program. Screen readers expect every item in it to be a menuitem, menuitemcheckbox or menuitemradio, sometimes grouped with group or split with separator. An <li> set to listitem, option, link, button or tab breaks that structure. The check covers every <li> under the menu, not only its direct children, so a nested list such as a language picker built as a listbox triggers it too.
An <li> with no role attribute passes, and so does one with role="none" or role="presentation". Only an explicit role outside the allowed list raises the error.
Usually the menu roles shouldn't be there at all. They tend to end up on a site's main navigation, which is just a list of links to other pages. A screen reader that announces a "menu bar" tells the user to move between items with the arrow keys, and few sites script that behavior. Remove role="menu", role="menubar" and the item roles, and keep a <nav> with a plain list of links. Save the menu roles for widgets that work like a desktop menu, where each item runs a command. There, each <li> is either a menu item itself or has role="none" and wraps one.
Invalid example
A language picker built as a listbox sits inside navigation marked up as a menubar. Each <li role="option"> triggers the error:
<nav aria-label="Main">
<ul role="menubar">
<li role="none"><a role="menuitem" href="/">Home</a></li>
<li role="none"><a role="menuitem" href="/news/">News</a></li>
<li role="none">
<ul role="listbox" aria-label="Language">
<li role="option" aria-selected="true">English</li>
<li role="option" aria-selected="false">Español</li>
</ul>
</li>
</ul>
</nav>
Valid example
Site navigation as plain lists of links, with no menu roles:
<nav aria-label="Main">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/news/">News</a></li>
</ul>
<ul aria-label="Language">
<li><a href="/" aria-current="true">English</a></li>
<li><a href="/es/" hreflang="es" lang="es">Español</a></li>
</ul>
</nav>
For a real application menu, every <li> gets a menu role or role="none". The items also need scripted keyboard support (arrow keys, Escape, focus handling), as the WAI-ARIA menubar pattern describes:
<ul role="menubar" aria-label="Editor">
<li role="none">
<button type="button" role="menuitem" aria-haspopup="menu" aria-expanded="true">Format</button>
<ul role="menu" aria-label="Format">
<li role="menuitemcheckbox" aria-checked="true" tabindex="-1">Bold</li>
<li role="menuitemcheckbox" aria-checked="false" tabindex="-1">Italic</li>
<li role="separator"></li>
<li role="menuitem" tabindex="-1">Clear formatting</li>
</ul>
</li>
</ul>
The alert ARIA role is used to communicate important, typically time-sensitive messages to the user. When an element has role="alert", assistive technologies like screen readers will immediately announce its content to the user, interrupting whatever they are currently doing. This makes it ideal for error messages, warnings, or status updates that require immediate attention.
However, not every HTML element can accept every ARIA role. The WHATWG HTML specification and WAI-ARIA in HTML define rules about which roles are allowed on which elements. The <ul> element has an implicit role of list, and the alert role is not among the roles permitted on <ul>. This restriction exists because overriding the semantic meaning of a list element with an alert role creates a conflict — assistive technologies would no longer convey the list structure to users, and the element's children (<li> elements) would lose their meaningful context as list items.
This matters for accessibility and standards compliance. If a screen reader encounters a <ul> with role="alert", the behavior becomes unpredictable. Some screen readers might announce it as an alert but fail to convey the list structure, while others might ignore the role entirely. Users who rely on assistive technology could miss either the alert or the list semantics, both of which may be important.
The fix depends on your intent. If you need to alert the user about content that happens to include a list, wrap the <ul> in a container element (like a <div>) and apply role="alert" to that container. If the content doesn't need to be a list, replace the <ul> with a more appropriate element like <div> or <p>.
Examples
❌ Invalid: role="alert" directly on a <ul>
<ul role="alert">
<li>Your password must be at least 8 characters.</li>
<li>Your password must contain a number.</li>
</ul>
This triggers the validation error because alert is not a valid role for the <ul> element.
✅ Fixed: Wrapping the list in a <div> with role="alert"
<div role="alert">
<ul>
<li>Your password must be at least 8 characters.</li>
<li>Your password must contain a number.</li>
</ul>
</div>
Here, the <div> carries the role="alert", so assistive technologies will announce the content immediately. The <ul> retains its native list semantics, and the <li> items are properly conveyed as list items.
✅ Fixed: Using a non-list element when list structure isn't needed
<div role="alert">
<p>Your session will expire in 2 minutes.</p>
</div>
If your alert content is a simple message rather than a list of items, use a more appropriate element like <p> or <div>.
✅ Fixed: Using aria-live as an alternative for dynamic updates
<div aria-live="assertive" role="alert">
<ul>
<li>Error: Email address is required.</li>
<li>Error: Name field cannot be empty.</li>
</ul>
</div>
The aria-live="assertive" attribute on the wrapper ensures that when the content is dynamically updated, assistive technologies announce the changes immediately. Combined with role="alert" on the wrapper (not the list), this provides robust accessible notifications while preserving list semantics.
Key points to remember
- The
role="alert"attribute cannot be placed on<ul>,<ol>, or<li>elements. - Always apply
role="alert"to a generic container element like<div>or<span>. - If your alert content includes a list, nest the list inside the alert container rather than making the list itself the alert.
- The
alertrole implicitly setsaria-live="assertive"andaria-atomic="true", so you don't need to add those separately when usingrole="alert".
The W3C HTML validator enforces rules about which ARIA roles can be applied to specific HTML elements. The <section> element carries implicit semantics — it maps to the ARIA region role when it has an accessible name (e.g., via aria-label or aria-labelledby). While article is indeed a valid ARIA role defined in the WAI-ARIA specification, the HTML specification restricts which roles can override a <section> element's native semantics. The allowed roles for <section> include alert, alertdialog, application, contentinfo, dialog, document, feed, log, main, marquee, navigation, none, note, presentation, search, status, tabpanel, and region — but not article.
This restriction exists because HTML already provides the <article> element, which carries the implicit article ARIA role natively. Using role="article" on a <section> creates a confusing mismatch: the element's tag name suggests one semantic meaning while the role attribute declares another. This can confuse assistive technologies like screen readers, which may announce the element inconsistently depending on whether they prioritize the tag name or the explicit role.
The best fix depends on your intent:
- If the content is self-contained and independently meaningful (like a blog post, comment, or news story), replace the
<section>with an<article>element. The<article>element already has the implicitarticlerole, so noroleattribute is needed. - If the content is a thematic grouping within a page, keep the
<section>element and remove theroleattribute. Give it a heading or anaria-labelso it functions as a meaningful landmark. - If you specifically need the
articlerole on a non-semantic element, use a<div>withrole="article"instead, since<div>has no implicit role and allows any ARIA role to be applied.
Examples
Incorrect: role="article" on a <section>
This triggers the validation error because article is not a permitted role for <section>.
<section role="article">
<h2>Breaking news</h2>
<p>Details about the event.</p>
</section>
Correct: use <article> for self-contained content
The <article> element has the implicit article role, making the explicit role attribute unnecessary.
<article>
<h2>Breaking news</h2>
<p>Details about the event.</p>
</article>
Correct: use <section> without a conflicting role
If the content is a thematic grouping rather than a standalone piece, keep <section> and drop the role attribute. Adding an accessible name via aria-labelledby makes it a region landmark.
<section aria-labelledby="news-heading">
<h2 id="news-heading">Latest news</h2>
<p>Details about the event.</p>
</section>
Correct: use a <div> when you need an explicit article role
In rare cases where you cannot use the <article> element but need the article role, a <div> accepts any valid ARIA role.
<div role="article">
<h2>Breaking news</h2>
<p>Details about the event.</p>
</div>
Correct: nest <article> inside <section> for grouped articles
If you need both a thematic grouping and individual self-contained items, nest <article> elements inside a <section>.
<section aria-labelledby="stories-heading">
<h2 id="stories-heading">Top stories</h2>
<article>
<h3>First story</h3>
<p>Story content.</p>
</article>
<article>
<h3>Second story</h3>
<p>Story content.</p>
</article>
</section>
As a general rule, prefer native HTML elements over ARIA roles whenever possible. The <article> element communicates the article role more reliably than any ARIA override, and it works consistently across all browsers and assistive technologies without additional attributes.
The role="button" attribute is not allowed on the article element because article already has an implicit ARIA role of article, and button is not among the roles permitted on this element.
The article element represents a self-contained composition in a document, such as a blog post, a news story, or a forum comment. Its implicit ARIA role is article. According to the ARIA in HTML specification, the article element can only use a limited set of roles: application, document, feed, main, none, presentation, or region. The button role is not in that list.
The button role turns an element into an interactive control that users can activate, which clashes with article, a container for standalone content. If you need a clickable control, use a real <button> element so you get keyboard support and focus handling for free. If you need the semantics of article, keep the element and remove the role.
Example with the issue
<article role="button">
<h2>Latest news</h2>
<p>Content of the article.</p>
</article>
How to fix it
If you need a control the user can activate, use a <button>:
<button type="button">Read the latest news</button>
If you need the semantics of article, drop the conflicting role:
<article>
<h2>Latest news</h2>
<p>Content of the article.</p>
</article>
The ARIA in HTML specification defines which roles are allowed on each HTML element. Heading elements (<h1>–<h6>) have an implicit role of heading, and the set of roles they can be explicitly assigned is limited. The button role is not among them, so applying role="button" directly to a heading element is invalid.
This matters for several reasons. First, headings play a critical role in document structure and accessibility — screen reader users rely on headings to navigate and understand the page hierarchy. Assigning role="button" to a heading overrides its semantic meaning, which confuses assistive technologies. Second, browsers and screen readers may handle conflicting semantics unpredictably, leading to an inconsistent experience for users. Third, it violates the W3C HTML specification, which means your markup won't pass validation.
Why This Combination Is Problematic
When you apply role="button" to an element, assistive technologies treat it as an interactive button. This completely replaces the element's native heading semantics. Users who navigate by headings would no longer find that heading in their list, and users who navigate by interactive controls would encounter a "button" that may not behave like one (lacking keyboard support, focus management, etc.).
If you genuinely need something that looks like a heading but acts as a button, there are valid approaches to achieve this without breaking semantics.
How to Fix It
There are several strategies depending on your intent:
Use a
<button>element styled as a heading. This is often the cleanest approach when the primary purpose is interactivity. You can style the button with CSS to match your heading appearance.Wrap the heading in a
<div>withrole="button". This preserves the heading in the document outline while making the wrapper interactive. However, be aware that thebuttonrole appliesrole="presentation"to all descendant elements, meaning assistive technologies will strip the heading semantics from the<h2>inside it. The text content remains accessible, but it won't be recognized as a heading.Place a
<button>inside the heading. This keeps the heading semantics intact for document structure while making the text inside it interactive. This pattern is commonly used for accordion-style components and is the approach recommended by the WAI-ARIA Authoring Practices.
Examples
❌ Invalid: role="button" on a heading
<h2 role="button">Toggle Section</h2>
This triggers the validation error because button is not an allowed role for heading elements.
✅ Fix: Use a <button> styled as a heading
<button type="button" class="heading-style">Toggle Section</button>
.heading-style {
font-size: 1.5em;
font-weight: bold;
background: none;
border: none;
cursor: pointer;
}
✅ Fix: Wrap the heading in a container with role="button"
<div role="button" tabindex="0">
<h2>Toggle Section</h2>
</div>
Note that this approach causes the <h2> to lose its heading semantics for assistive technologies, since the button role does not support semantic children. Also remember to add tabindex="0" so the element is keyboard-focusable, and implement keydown handlers for Enter and Space to replicate native button behavior.
✅ Fix: Place a <button> inside the heading (recommended for accordions)
<h2>
<button type="button" aria-expanded="false">
Toggle Section
</button>
</h2>
This is the most robust pattern. The heading remains in the document outline, and the button inside it is fully interactive with built-in keyboard support. Screen reader users can find it both when navigating by headings and when navigating by interactive elements. The aria-expanded attribute communicates whether the associated section is open or closed.
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.
1K Weekly
1,000 monthly checks. Cancel anytime.
Get 1K Weekly →Join teams across 40+ countries