HTML Guides for aria
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.
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.
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>
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.
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 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>
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>
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>
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>
An <li> element inside a <ul>, <ol>, or <menu> must not be assigned any ARIA role other than listitem.
When an <li> sits inside a standard list container (<ul>, <ol>, or <menu>) that has no explicit role attribute, the browser already treats the <li> as having an implicit role of listitem. Assigning a different role, such as role="presentation", role="option", or role="tab", contradicts the semantics of the parent-child relationship between the list and its items. Screen readers and other assistive technologies rely on this relationship to convey list structure to users.
The same restriction applies when the parent element has role="list" set explicitly. In that context, child <li> elements are still expected to carry only the listitem role.
If you need a different role on the items, restructure the markup. For example, use <div> elements instead of <ul> and <li>, and apply the appropriate roles directly. Or, if the items genuinely are list items, remove the conflicting role attribute.
Examples
Invalid: li with a non-listitem role inside a ul
<ul>
<li role="tab">Dashboard</li>
<li role="tab">Settings</li>
<li role="tab">Profile</li>
</ul>
Fixed: use appropriate elements when a different role is needed
If the items are tabs, drop the list markup and use elements that match the intended semantics:
<div role="tablist">
<button role="tab" aria-selected="true">Dashboard</button>
<button role="tab" aria-selected="false">Settings</button>
<button role="tab" aria-selected="false">Profile</button>
</div>
Fixed: keep the list but remove the conflicting role
If the content really is a list, remove the extra role:
<ul>
<li>Dashboard</li>
<li>Settings</li>
<li>Profile</li>
</ul>
A control with the disabled attribute is already exposed as disabled, so aria-disabled repeats the same information and can be removed.
The disabled attribute works on <button>, <input>, <select>, <textarea>, <fieldset>, <optgroup> and <option>. Browsers take a disabled control out of the tab order, ignore clicks on it, and tell assistive technologies that it is disabled. Adding aria-disabled="true" says the same thing again. The ARIA in HTML specification advises against it, and the validator reports it as a warning.
You also get this warning when aria-disabled copies the value of disabled, as in disabled="disabled" aria-disabled="disabled", or when an empty aria-disabled="" sits next to a bare disabled. The validator then adds a Bad value error as well, because aria-disabled only accepts true or false.
Remove aria-disabled and keep disabled. If aria-disabled is set to false on an element with disabled, the two attributes contradict each other, and the validator reports that as an error instead.
Sometimes a button should stay focusable while it is unavailable, so keyboard and screen reader users can still reach it and find out why. For that, drop disabled and keep only aria-disabled="true". The validator accepts this, but aria-disabled changes nothing about behavior: the button still responds to clicks and still submits the form, so your script has to ignore those activations while aria-disabled is true.
Invalid example
<form action="/checkout" method="post">
<label for="email">Email</label>
<input type="email" id="email" name="email">
<button type="submit" disabled aria-disabled="true">Place order</button>
</form>
Valid example
<form action="/checkout" method="post">
<label for="email">Email</label>
<input type="email" id="email" name="email">
<button type="submit" disabled>Place order</button>
</form>
A field with the readonly attribute is already exposed as read-only, so aria-readonly repeats the same information and can be removed.
The readonly attribute on an <input> or <textarea> tells browsers and assistive technologies that the user can't change the value. Adding aria-readonly="true" states the same thing a second time. The ARIA in HTML specification advises against it, and the validator reports it as a warning.
The same warning appears when aria-readonly repeats the exact value of readonly, as in readonly="readonly" aria-readonly="readonly", or an empty aria-readonly="" next to a bare readonly. In those cases the validator also reports a Bad value error, because aria-readonly only accepts true or false.
Remove aria-readonly and keep readonly. aria-readonly is meant for custom widgets, such as an element with role="textbox" or a grid cell, which have no native readonly attribute. If aria-readonly is set to false on a field with readonly, the two attributes contradict each other, and the validator reports that as an error instead.
Invalid example
<label for="account">Account ID</label>
<input type="text" id="account" name="account" value="AC-1042" readonly aria-readonly="true">
Valid example
<label for="account">Account ID</label>
<input type="text" id="account" name="account" value="AC-1042" readonly>
A field with the readonly attribute can't also have aria-readonly="false": remove aria-readonly and let the native attribute set the state.
The readonly attribute on an <input> or <textarea> already tells browsers and assistive technologies that the user can't change the value. aria-readonly="false" claims the opposite, so the field exposes two conflicting states. The ARIA in HTML specification forbids aria-readonly="false" on any element that has readonly, and the validator reports it as an error.
This often comes from a component or template that always outputs aria-readonly from a flag that defaults to false, while readonly is added separately. Decide what the field should be. If users shouldn't edit it, keep readonly and drop aria-readonly. If they should, remove readonly. aria-readonly is meant for custom widgets, such as an element with role="textbox" or a grid cell, which have no native readonly attribute.
Writing aria-readonly="true" next to readonly doesn't trigger this error. That combination is only redundant, and the validator reports it as a warning.
Invalid example
<label for="account">Account ID</label>
<input type="text" id="account" name="account" value="AC-1042" readonly aria-readonly="false">
Valid example
<label for="account">Account ID</label>
<input type="text" id="account" name="account" value="AC-1042" readonly>
A field with the readonly attribute has an aria-readonly value that doesn't match it: remove aria-readonly and keep the native attribute.
aria-readonly only accepts true or false. A value such as TRUE, 1, undefined or readonly is neither, so the validator reports two errors on the same field: a Bad value error for aria-readonly, and this one, because the value differs from what readonly states. Values like these usually come from a template that prints a variable straight into the attribute, for example a boolean that the template language writes as 1 or True.
The readonly attribute already exposes the read-only state to assistive technologies, so the field doesn't need aria-readonly at all. Remove it. If you're building a custom widget that has no native readonly attribute, such as an element with role="textbox", write aria-readonly as exactly true or false.
Other values give different messages. aria-readonly="false" next to readonly is its own error, because it contradicts the native attribute. aria-readonly="true", or a value that exactly repeats the value of readonly (such as readonly="readonly" aria-readonly="readonly"), only gets a warning that the attribute is unnecessary.
Invalid example
<label for="account">Account ID</label>
<input type="text" id="account" name="account" value="AC-1042" readonly aria-readonly="1">
Valid example
<label for="account">Account ID</label>
<input type="text" id="account" name="account" value="AC-1042" readonly>
A field with the required attribute has an aria-required value that doesn't match it: remove aria-required and keep the native attribute.
aria-required only accepts true or false. A value such as yes, 1 or required is neither, so the validator reports two errors on the same field: a Bad value error for aria-required, and this one, because the value differs from what required states. These values usually come from copying the native attribute into the ARIA one, as in aria-required="required", or from a template that prints a boolean as 1 or True.
The required attribute already tells assistive technologies that the field must be filled in, and it turns on the browser's own form validation. The field doesn't need aria-required at all, so remove it. If you're building a custom widget that can't take required, such as an element with role="combobox", write aria-required as exactly true or false.
Other values give different messages. aria-required="false" next to required is its own error, because it contradicts the native attribute. aria-required="true", or a value that exactly repeats the value of required (such as required="required" aria-required="required"), only gets a warning that the attribute is unnecessary.
Invalid example
<label for="email">Email</label>
<input type="email" id="email" name="email" required aria-required="yes">
Valid example
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
The aria-labelledby attribute is part of the WAI-ARIA specification and provides an accessible name for an element by referencing the id values of other elements that contain the labeling text. Without the aria- prefix, labelledby is simply an unrecognized attribute that browsers and assistive technologies will ignore. This means your SVG graphic won't have the accessible label you intended, leaving screen reader users without a meaningful description of the content.
This issue is especially important for <svg> elements because SVG graphics are often used for icons, charts, and illustrations that need descriptive labels for accessibility. Using the incorrect attribute name means the graphic is effectively unlabeled for users who rely on assistive technology.
How to Fix It
Replace labelledby with aria-labelledby on your <svg> element. The attribute's value should be a space-separated list of one or more id values that reference elements containing the label text.
If you want to label an SVG using text that's already visible on the page, aria-labelledby is the ideal approach. You can also reference a <title> element inside the SVG itself.
Examples
❌ Incorrect: Using labelledby (invalid attribute)
<h2 id="chart-title">Monthly Sales</h2>
<svg labelledby="chart-title" role="img" viewBox="0 0 200 100">
<!-- chart content -->
</svg>
✅ Correct: Using aria-labelledby to reference an external heading
<h2 id="chart-title">Monthly Sales</h2>
<svg aria-labelledby="chart-title" role="img" viewBox="0 0 200 100">
<!-- chart content -->
</svg>
✅ Correct: Using aria-labelledby to reference the SVG's own <title>
<svg aria-labelledby="icon-title" role="img" viewBox="0 0 24 24">
<title id="icon-title">Search</title>
<path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5z"/>
</svg>
✅ Correct: Referencing multiple label sources
You can combine multiple id values to build a composite accessible name, separated by spaces:
<h2 id="section-title">Revenue</h2>
<p id="section-desc">Q1 2024 revenue by region</p>
<svg aria-labelledby="section-title section-desc" role="img" viewBox="0 0 400 200">
<!-- chart content -->
</svg>
In this case, a screen reader would announce something like "Revenue Q1 2024 revenue by region" as the accessible name for the SVG.
Tips
- When using
aria-labelledbyon<svg>, also addrole="img"to ensure consistent behavior across screen readers. - If the SVG is purely decorative, use
aria-hidden="true"instead of labeling it. - The
aria-labelledbyattribute overrides other labeling mechanisms likearia-labelor the<title>element, so use it when you want a specific label to take precedence.
The aria-rowspan attribute is used in ARIA-based grid and table structures to indicate how many rows a cell spans. It serves a similar purpose to the rowspan attribute on native HTML <td> and <th> elements, but is designed for custom widgets built with ARIA roles like gridcell, rowheader, and columnheader within grid or treegrid structures.
According to the WAI-ARIA specification, the value of aria-rowspan must be a positive integer — a whole number greater than zero. A value of "0" is invalid because it implies the cell spans no rows, which is semantically meaningless. Note that this differs from native HTML's rowspan attribute, where "0" has a special meaning (span all remaining rows in the row group). The ARIA attribute does not support this behavior.
This matters primarily for accessibility. Screen readers and other assistive technologies rely on aria-rowspan to convey the structure of custom grids to users. An invalid value of "0" can confuse assistive technology, potentially causing it to misrepresent the grid layout or skip the cell entirely. Ensuring valid values helps users who depend on these tools navigate your content correctly.
To fix this issue, determine how many rows the cell actually spans and set aria-rowspan to that number. If the cell occupies a single row, use "1". If it spans multiple rows, use the appropriate count. If you don't need row spanning at all, you can simply remove the aria-rowspan attribute entirely, since the default behavior is to span one row.
Examples
Incorrect: aria-rowspan set to zero
<div role="grid">
<div role="row">
<div role="gridcell" aria-rowspan="0">Name</div>
<div role="gridcell">Value</div>
</div>
</div>
The value "0" is not a positive integer, so the validator reports an error.
Correct: aria-rowspan set to a positive integer
If the cell spans a single row, use "1" (or remove the attribute, since one row is the default):
<div role="grid">
<div role="row">
<div role="gridcell" aria-rowspan="1">Name</div>
<div role="gridcell">Value</div>
</div>
</div>
Correct: aria-rowspan for a cell spanning multiple rows
If the cell genuinely spans two rows, set the value accordingly:
<div role="grid">
<div role="row">
<div role="gridcell" aria-rowspan="2">Category</div>
<div role="gridcell">Item A</div>
</div>
<div role="row">
<div role="gridcell">Item B</div>
</div>
</div>
Correct: removing the attribute when no spanning is needed
If the cell doesn't span multiple rows, the simplest fix is to remove aria-rowspan altogether:
<div role="grid">
<div role="row">
<div role="gridcell">Name</div>
<div role="gridcell">Value</div>
</div>
</div>
When to use aria-rowspan vs. native HTML
If you're building a data table, prefer native HTML <table>, <tr>, <td>, and <th> elements with the standard rowspan attribute. Native table semantics are automatically understood by browsers and assistive technologies without any ARIA attributes. Reserve aria-rowspan for custom interactive widgets (like spreadsheet-style grids or tree grids) where native table elements aren't appropriate. The aria-rowspan value should always match the actual visual and structural layout of your grid to avoid confusing assistive technology users.
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