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>
<buttononclick="switchView('home')">Home</button>
<buttononclick="switchView('about')">About</button>
</nav>
</header>
<mainid="home">
<h2>Welcome</h2>
<p>This is the home page content.</p>
</main>
<mainid="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>
<mainstyle="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:
<ahref="/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:
<ahref="/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:
<aaria-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:
<buttontype="button"disabled>Perform Action</button>
This is the simplest and most robust solution when the element doesn't need to be a link. The disabled attribute is natively understood by browsers and assistive technologies without any ARIA workarounds.
The ARIA specification defines a strict ownership hierarchy for table-related roles. A 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:
<divrole="table"aria-label="Employees">
<divrole="columnheader">Name</div>
<divrole="columnheader">Department</div>
<divrole="row">
<divrole="cell">Alice</div>
<divrole="cell">Engineering</div>
</div>
</div>
Correct: columnheader inside a row
Wrapping the column headers in an element with role="row" fixes the issue:
<divrole="table"aria-label="Employees">
<divrole="row">
<divrole="columnheader">Name</div>
<divrole="columnheader">Department</div>
</div>
<divrole="row">
<divrole="cell">Alice</div>
<divrole="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:
<divrole="table"aria-label="Employees">
<divrole="rowgroup">
<divrole="row">
<divrole="columnheader">Name</div>
<divrole="columnheader">Department</div>
</div>
</div>
<divrole="rowgroup">
<divrole="row">
<divrole="cell">Alice</div>
<divrole="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 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.
<divrole="listitem">Apples</div>
<divrole="listitem">Bananas</div>
<divrole="listitem">Cherries</div>
Correct: wrapping items in role="list"
Adding a parent container with role="list" establishes the required ownership relationship.
<divrole="list">
<divrole="listitem">Apples</div>
<divrole="listitem">Bananas</div>
<divrole="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.
<divrole="list">
<divrole="listitem">Fruits
<divrole="group">
<divrole="listitem">Apples</div>
<divrole="listitem">Bananas</div>
</div>
</div>
<divrole="listitem">Vegetables
<divrole="group">
<divrole="listitem">Carrots</div>
<divrole="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.
<divrole="list"aria-owns="item-1 item-2 item-3"></div>
<!-- These items live elsewhere in the DOM -->
<divrole="listitem"id="item-1">Apples</div>
<divrole="listitem"id="item-2">Bananas</div>
<divrole="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>
<divrole="menuitem">Cut</div>
<divrole="menuitem">Copy</div>
<divrole="menuitem">Paste</div>
</div>
Correct — menuitem inside a menu
Wrapping the items in an element with role="menu" resolves the issue:
<divrole="menu">
<divrole="menuitem"tabindex="0">Cut</div>
<divrole="menuitem"tabindex="-1">Copy</div>
<divrole="menuitem"tabindex="-1">Paste</div>
</div>
Correct — menuitem inside a menubar
For a persistent horizontal menu bar with application-style actions:
<divrole="menubar">
<divrole="menuitem"tabindex="0">File</div>
<divrole="menuitem"tabindex="-1">Edit</div>
<divrole="menuitem"tabindex="-1">View</div>
</div>
Correct — nested menus with dropdown submenus
A menubar with a dropdown menu containing additional menuitem elements:
<divrole="menubar">
<divrole="menuitem"tabindex="0"aria-haspopup="true"aria-expanded="false">
File
<divrole="menu">
<divrole="menuitem"tabindex="-1">New</div>
<divrole="menuitem"tabindex="-1">Open</div>
<divrole="menuitem"tabindex="-1">Save</div>
</div>
</div>
<divrole="menuitem"tabindex="-1"aria-haspopup="true"aria-expanded="false">
Edit
<divrole="menu">
<divrole="menuitem"tabindex="-1">Cut</div>
<divrole="menuitem"tabindex="-1">Copy</div>
<divrole="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:
<divrole="menu"aria-owns="item-cut item-copy item-paste"></div>
<divrole="menuitem"id="item-cut"tabindex="0">Cut</div>
<divrole="menuitem"id="item-copy"tabindex="-1">Copy</div>
<divrole="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:
<navaria-label="Main navigation">
<ul>
<li><ahref="/">Home</a></li>
<li><ahref="/about">About</a></li>
<li><ahref="/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.
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.
<divclass="tabs">
<divrole="tablist"aria-label="Sample Tabs"></div>
<buttonrole="tab"aria-selected="true"aria-controls="panel-1"id="tab-1">
First Tab
</button>
<buttonrole="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.
<divclass="tabs">
<divrole="tablist"aria-label="Sample Tabs">
<buttonrole="tab"aria-selected="true"aria-controls="panel-1"id="tab-1"tabindex="0">
First Tab
</button>
<buttonrole="tab"aria-selected="false"aria-controls="panel-2"id="tab-2"tabindex="-1">
Second Tab
</button>
</div>
<divid="panel-1"role="tabpanel"tabindex="0"aria-labelledby="tab-1">
<p>Content for the first panel</p>
</div>
<divid="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.
<divclass="tabs">
<divrole="tablist"aria-label="Sample Tabs"aria-owns="tab-1 tab-2"></div>
<divclass="tab-wrapper">
<buttonrole="tab"aria-selected="true"aria-controls="panel-1"id="tab-1"tabindex="0">
First Tab
</button>
<buttonrole="tab"aria-selected="false"aria-controls="panel-2"id="tab-2"tabindex="-1">
Second Tab
</button>
</div>
<divid="panel-1"role="tabpanel"tabindex="0"aria-labelledby="tab-1">
<p>Content for the first panel</p>
</div>
<divid="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.
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
<formaction="/submit"method="post">
<inputtype="hidden"name="referer"value="https://example.com"aria-invalid="false">
<buttontype="submit">Submit</button>
</form>
Correct: hidden input without aria-* attributes
<formaction="/submit"method="post">
<inputtype="hidden"name="referer"value="https://example.com">
<buttontype="submit">Submit</button>
</form>
Incorrect: hidden input with multiple aria-* attributes
<formaction="/save"method="post">
<input
type="hidden"
name="session_token"
value="abc123"
aria-label="Session token"
aria-required="true"
aria-describedby="token-help">
<buttontype="submit">Save</button>
</form>
Correct: all aria-* attributes removed
<formaction="/save"method="post">
<inputtype="hidden"name="session_token"value="abc123">
<buttontype="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":
<formaction="/login"method="post">
<labelfor="username">Username</label>
<input
type="text"
id="username"
name="username"
aria-required="true"
aria-invalid="false"
aria-describedby="username-help">
<pid="username-help">Enter your registered email or username.</p>
<buttontype="submit">Log in</button>
</form>
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)
<h2id="chart-title">Monthly Sales</h2>
<svglabelledby="chart-title"role="img"viewBox="0 0 200 100">
<!-- chart content -->
</svg>
✅ Correct: Using aria-labelledby to reference an external heading
<h2id="chart-title">Monthly Sales</h2>
<svgaria-labelledby="chart-title"role="img"viewBox="0 0 200 100">
<!-- chart content -->
</svg>
✅ Correct: Using aria-labelledby to reference the SVG's own <title>
<svgaria-labelledby="icon-title"role="img"viewBox="0 0 24 24">
<titleid="icon-title">Search</title>
<pathd="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:
<h2id="section-title">Revenue</h2>
<pid="section-desc">Q1 2024 revenue by region</p>
<svgaria-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
<divrole="grid">
<divrole="row">
<divrole="gridcell"aria-rowspan="0">Name</div>
<divrole="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):
<divrole="grid">
<divrole="row">
<divrole="gridcell"aria-rowspan="1">Name</div>
<divrole="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:
<divrole="grid">
<divrole="row">
<divrole="gridcell"aria-rowspan="2">Category</div>
<divrole="gridcell">Item A</div>
</div>
<divrole="row">
<divrole="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:
<divrole="grid">
<divrole="row">
<divrole="gridcell">Name</div>
<divrole="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.
The aria-setsize attribute tells assistive technologies how many items exist in a set of related elements (such as list items or tree items). It is particularly useful when not all items in a set are present in the DOM — for example, in virtualized lists, paginated results, or lazy-loaded content. According to the WAI-ARIA 1.2 specification:
Authors MUST set the value of
aria-setsizeto an integer equal to the number of items in the set. If the total number of items is unknown, authors SHOULD set the value ofaria-setsizeto-1.
This means -1 is a specifically recommended value for cases where the total count of items is indeterminate. The W3C validator's pattern matching expects only non-negative digits, so it rejects the leading - character. This is a known validator bug and does not reflect an actual problem with your HTML.
Why -1 matters for accessibility
When building interfaces with dynamic or partially loaded content, screen readers need to communicate the size of a set to users. If you have a list of search results but don't know the total count, setting aria-setsize="-1" tells assistive technologies that the set size is unknown. Without this, a screen reader might announce incorrect or misleading information about how many items exist.
What you should do
Do not change your markup to work around this validator error. The value -1 is correct and serves an important accessibility purpose. Removing it or replacing it with an arbitrary positive number would degrade the experience for users of assistive technologies.
Examples
Valid usage that triggers the false positive
This markup is correct but will be flagged by the validator:
<ul>
<lirole="option"aria-setsize="-1"aria-posinset="1">Result A</li>
<lirole="option"aria-setsize="-1"aria-posinset="2">Result B</li>
<lirole="option"aria-setsize="-1"aria-posinset="3">Result C</li>
</ul>
Here, the total number of results is unknown (perhaps they are loaded on demand), so aria-setsize="-1" correctly signals this to assistive technologies.
Known set size (no validator error)
When the total number of items is known, use the actual count. This will not trigger the validator error:
<ul>
<lirole="option"aria-setsize="5"aria-posinset="1">Item 1</li>
<lirole="option"aria-setsize="5"aria-posinset="2">Item 2</li>
<lirole="option"aria-setsize="5"aria-posinset="3">Item 3</li>
</ul>
When aria-setsize is not needed
If all items in the set are present in the DOM, you don't need aria-setsize at all — the browser can compute the set size automatically:
<ulrole="listbox">
<lirole="option">Apple</li>
<lirole="option">Banana</li>
<lirole="option">Cherry</li>
</ul>
In summary, if you see this validator error and you're intentionally using aria-setsize="-1" because the total item count is unknown, your code is correct. You can safely ignore this particular warning.
The selected attribute on the <option> element is a boolean attribute and does not accept a value like "1".
Boolean attributes in HTML don't need a value at all. Their mere presence on an element means "true," and their absence means "false." When you write selected="1", the W3C validator flags it because "1" is not a valid value for a boolean attribute.
According to the HTML specification, a boolean attribute can only have three valid forms: the attribute name alone (selected), an empty string (selected=""), or the attribute's own name as the value (selected="selected"). Any other value, including "1", "true", or "yes", is invalid.
Invalid Example
<selectname="color">
<optionvalue="red">Red</option>
<optionvalue="blue"selected="1">Blue</option>
<optionvalue="green">Green</option>
</select>
Valid Example
<selectname="color">
<optionvalue="red">Red</option>
<optionvalue="blue"selected>Blue</option>
<optionvalue="green">Green</option>
</select>
Using just selected without a value is the cleanest and most common approach.
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>
<ulrole="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"
<divrole="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
<divrole="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
<divaria-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 aria-expanded attribute communicates to assistive technologies whether a related grouping element (such as a dropdown menu, accordion panel, or collapsible section) is currently expanded or collapsed. It accepts only three valid values:
"true"— the controlled element is expanded and visible."false"— the controlled element is collapsed and hidden."undefined"— the element has no expandable relationship (this is also the implicit default when the attribute is omitted entirely).
This validation error typically occurs when the attribute is accidentally set to a non-boolean value. A common mistake is writing aria-expanded="aria-expanded", which mimics the old HTML4 pattern for boolean attributes like checked="checked". However, aria-expanded is not a standard HTML boolean attribute — it is an ARIA state attribute that requires an explicit string value of "true" or "false".
Setting an invalid value means assistive technologies like screen readers cannot correctly interpret the state of the control. A screen reader user may not know whether a menu is open or closed, leading to a confusing and inaccessible experience. Browsers may also handle the invalid value unpredictably, potentially treating it as truthy or ignoring it altogether.
How to fix it
- Identify the element with the invalid
aria-expandedvalue. - Replace the value with
"true"if the associated content is currently expanded, or"false"if it is collapsed. - If the button has no expand/collapse relationship at all, remove the
aria-expandedattribute entirely. - Ensure that JavaScript toggling logic updates the attribute to
"true"or"false"— never to any other string.
Examples
❌ Invalid: attribute set to a non-boolean string
<buttonaria-expanded="aria-expanded"aria-controls="menu">
Toggle Menu
</button>
<ulid="menu">
<li>Option 1</li>
<li>Option 2</li>
</ul>
The value "aria-expanded" is not a recognized value and triggers the validation error.
✅ Fixed: attribute set to "false" (collapsed state)
<buttonaria-expanded="false"aria-controls="menu">
Toggle Menu
</button>
<ulid="menu"hidden>
<li>Option 1</li>
<li>Option 2</li>
</ul>
✅ Fixed: attribute set to "true" (expanded state)
<buttonaria-expanded="true"aria-controls="menu">
Toggle Menu
</button>
<ulid="menu">
<li>Option 1</li>
<li>Option 2</li>
</ul>
❌ Invalid: other common incorrect values
<!-- Using "yes" instead of "true" -->
<buttonaria-expanded="yes">Details</button>
<!-- Using "1" instead of "true" -->
<buttonaria-expanded="1">Details</button>
<!-- Empty value -->
<buttonaria-expanded="">Details</button>
All of these are invalid. The only accepted values are "true", "false", and "undefined".
✅ Toggling with JavaScript
When toggling aria-expanded dynamically, make sure the value is always set to the correct string:
<buttonaria-expanded="false"aria-controls="panel"onclick="togglePanel(this)">
Show details
</button>
<divid="panel"hidden>
<p>Additional details here.</p>
</div>
<script>
functiontogglePanel(button){
constexpanded=button.getAttribute("aria-expanded")==="true";
button.setAttribute("aria-expanded",String(!expanded));
constpanel=document.getElementById(button.getAttribute("aria-controls"));
panel.hidden=expanded;
}
</script>
This ensures the attribute always toggles between "true" and "false", keeping the markup valid and the experience accessible for all users.
The combobox role represents a composite widget that combines a text input with a popup (typically a listbox) that helps the user set the value of the input. It's a common pattern seen in autocomplete fields, search suggestions, and dropdown selects with type-ahead functionality.
The reason the validator rejects role="combobox" on <input> is rooted in how the HTML specification defines allowed ARIA roles for each element. An <input type="text"> already carries an implicit role of textbox, and the spec only permits a limited set of explicit roles on it (such as combobox in ARIA 1.2 contexts, but W3C HTML validation may still flag this depending on the validator's conformance rules). When the validator encounters a role that isn't in its allowed list for that element, it raises this error.
Why This Matters
- Standards compliance: Using roles outside the permitted set for an element violates the HTML specification, which can lead to unpredictable behavior across browsers and assistive technologies.
- Accessibility: Assistive technologies like screen readers rely on correct role assignments to convey the widget's purpose. A misapplied role can confuse users who depend on these tools, making the combobox harder to navigate or understand.
- Browser interop: Browsers may handle conflicting implicit and explicit roles inconsistently, leading to different experiences across platforms.
How to Fix It
There are two main approaches depending on which ARIA pattern you follow:
Approach 1: ARIA 1.1 Pattern (Container as Combobox)
In the ARIA 1.1 combobox pattern, the role="combobox" is placed on a container element (like a <div>) that wraps the <input>. This is the approach most likely to pass W3C validation without issues.
Approach 2: ARIA 1.2 Pattern (Input as Combobox)
ARIA 1.2 moved the combobox role directly onto the <input> element itself, which is a simpler and more widely adopted pattern in practice. However, the W3C HTML Validator may still flag this as an error if its conformance rules haven't been updated to reflect ARIA 1.2. If passing validation is a strict requirement, use Approach 1.
Examples
❌ Incorrect: role="combobox" directly on <input>
This triggers the validation error:
<inputtype="text"role="combobox"aria-autocomplete="list">
✅ Correct: ARIA 1.1 pattern with container element
The role="combobox" is placed on the wrapping <div>, and the <input> inside it handles text entry:
<divrole="combobox"aria-haspopup="listbox"aria-owns="suggestions"aria-expanded="false">
<inputtype="text"aria-autocomplete="list"aria-controls="suggestions">
</div>
<ulid="suggestions"role="listbox"hidden>
<lirole="option"id="opt-1">Apple</li>
<lirole="option"id="opt-2">Banana</li>
<lirole="option"id="opt-3">Cherry</li>
</ul>
Key attributes explained:
role="combobox"on the<div>defines the overall widget for assistive technologies.aria-haspopup="listbox"tells screen readers that this widget has a popup of typelistbox.aria-owns="suggestions"establishes an ownership relationship between the combobox and the listbox, even if they aren't parent-child in the DOM.aria-expanded="false"indicates whether the popup is currently visible. Update this to"true"via JavaScript when the list is shown.aria-autocomplete="list"on the<input>signals that suggestions will be presented in a list as the user types.aria-controls="suggestions"on the<input>links it to the listbox it controls.
✅ Correct: ARIA 1.2 pattern using <input> with role="combobox" (if validation is not strict)
If your project follows ARIA 1.2 and you can tolerate or suppress the validation warning, this pattern is widely supported by modern browsers and screen readers:
<labelfor="fruit">Choose a fruit</label>
<input
id="fruit"
type="text"
role="combobox"
aria-autocomplete="list"
aria-expanded="false"
aria-controls="fruit-list"
aria-haspopup="listbox">
<ulid="fruit-list"role="listbox"hidden>
<lirole="option"id="fruit-1">Apple</li>
<lirole="option"id="fruit-2">Banana</li>
<lirole="option"id="fruit-3">Cherry</li>
</ul>
This is the pattern recommended by the WAI-ARIA Authoring Practices Guide and is the most commonly implemented in modern component libraries. If validation compliance is required, wrap the input in a container and move the role="combobox" there as shown in Approach 1.
Whichever approach you choose, remember that ARIA attributes alone don't create behavior — you'll need JavaScript to toggle aria-expanded, manage focus, handle keyboard navigation, and update aria-activedescendant as the user moves through options.
The HTML specification defines a set of implicit ARIA roles (also called "native semantics") for many HTML elements. The <dialog> element's implicit role is dialog, which means assistive technologies like screen readers already announce it correctly without any explicit ARIA markup. When you add role="dialog" to a <dialog> element, you're restating what the browser and accessibility tree already know—and the ARIA in HTML specification explicitly restricts this.
The ARIA in HTML spec maintains a list of allowed roles for each HTML element. For <dialog>, the only permitted role override is alertdialog (for dialogs that require an immediate response from the user). Setting role="dialog" is not listed as an allowed value because it duplicates the native semantics, and the spec treats such redundancy as a conformance error. This is why the W3C Validator reports: Bad value "dialog" for attribute "role" on element "dialog".
Why this matters
- Standards compliance: The W3C Validator enforces the ARIA in HTML specification, which prohibits redundant role assignments on elements that already carry that role implicitly. Valid markup ensures your pages conform to web standards.
- Accessibility clarity: While most assistive technologies handle redundant roles gracefully today, unnecessary ARIA attributes add noise to the codebase and can cause confusion about whether the element's native semantics are intentionally being overridden. The first rule of ARIA is: don't use ARIA if a native HTML element already provides the semantics you need.
- Maintainability: Removing redundant attributes keeps your HTML clean and easier to maintain. Future developers won't need to wonder whether the explicit role was added intentionally to work around a bug.
How to fix it
- Locate any
<dialog>element with arole="dialog"attribute. - Remove the
roleattribute entirely. - If you need the dialog to behave as an alert dialog (one that interrupts the user and demands immediate attention), use
role="alertdialog"instead—this is the one permitted role override for<dialog>.
Examples
Incorrect — redundant role causes a validation error
<dialogrole="dialog">
<h2>Confirm action</h2>
<p>Are you sure you want to proceed?</p>
<button>Cancel</button>
<button>Confirm</button>
</dialog>
Correct — relying on the implicit role
<dialog>
<h2>Confirm action</h2>
<p>Are you sure you want to proceed?</p>
<button>Cancel</button>
<button>Confirm</button>
</dialog>
The <dialog> element automatically exposes role="dialog" in the accessibility tree, so no explicit attribute is needed.
Correct — using an allowed role override
If the dialog represents an urgent alert that requires immediate user interaction, you can override the role with alertdialog:
<dialogrole="alertdialog"aria-labelledby="alert-title"aria-describedby="alert-desc">
<h2id="alert-title">Session expiring</h2>
<pid="alert-desc">Your session will expire in 60 seconds. Do you want to continue?</p>
<button>Stay signed in</button>
</dialog>
This is valid because alertdialog is explicitly listed as a permitted role for the <dialog> element in the ARIA in HTML specification. Note that aria-labelledby and aria-describedby are strongly recommended for alert dialogs so assistive technologies can announce the title and description properly.
The aria-activedescendant attribute tells assistive technologies which child element within a composite widget — such as a combobox, listbox, or autocomplete dropdown — is currently "active" or focused. Instead of moving actual DOM focus to each option, the parent element (like an input) retains focus while aria-activedescendant points to the visually highlighted option by referencing its id. This allows screen readers to announce the active option without disrupting keyboard interaction on the input.
When aria-activedescendant is set to an empty string (""), it creates an invalid state. The HTML and ARIA specifications require that any ID reference attribute either contains a valid, non-empty ID token or is omitted altogether. An empty string is not a valid ID, so the W3C validator flags this as an error: Bad value "" for attribute "aria-activedescendant" on element "input": An ID must not be the empty string.
This problem commonly occurs in JavaScript-driven widgets where aria-activedescendant is cleared by setting it to "" when no option is highlighted — for example, when a dropdown closes or the user clears their selection. While the developer's intent is correct (indicating that nothing is active), the implementation is wrong.
Why this matters
- Accessibility: Screen readers may behave unpredictably when encountering an empty ID reference. Some may silently ignore it, while others may announce errors or fail to convey widget state correctly.
- Standards compliance: The ARIA specification explicitly requires ID reference values to be non-empty strings that match an existing element's
id. - Browser consistency: Browsers handle invalid ARIA attributes inconsistently, which can lead to different experiences across platforms and assistive technologies.
How to fix it
- Remove the attribute when no descendant is active. Use
removeAttribute('aria-activedescendant')in JavaScript instead of setting it to an empty string. - Set a valid ID when a descendant becomes active, pointing to the
idof the currently highlighted or selected option. - Never render the attribute in HTML with an empty value. If your framework or templating engine conditionally renders attributes, ensure it omits the attribute entirely rather than outputting
aria-activedescendant="".
Examples
Incorrect: empty string value
This triggers the W3C validation error because the attribute value is an empty string.
<inputtype="text"role="combobox"aria-activedescendant=""/>
Correct: attribute omitted when no option is active
When nothing is active, simply leave the attribute off.
<inputtype="text"role="combobox"aria-expanded="false"/>
Correct: valid ID reference when an option is active
When a user highlights an option, set aria-activedescendant to that option's id.
<divrole="combobox">
<input
type="text"
role="combobox"
aria-expanded="true"
aria-controls="suggestions"
aria-activedescendant="option2"/>
<ulid="suggestions"role="listbox">
<liid="option1"role="option">Apple</li>
<liid="option2"role="option"aria-selected="true">Banana</li>
<liid="option3"role="option">Cherry</li>
</ul>
</div>
Correct: managing the attribute dynamically with JavaScript
The key fix in JavaScript is using removeAttribute instead of setting the value to an empty string.
<divrole="combobox">
<input
id="search"
type="text"
role="combobox"
aria-expanded="true"
aria-controls="results"/>
<ulid="results"role="listbox">
<liid="result1"role="option">First result</li>
<liid="result2"role="option">Second result</li>
</ul>
</div>
<script>
constinput=document.getElementById('search');
functionsetActiveOption(optionId){
if(optionId){
input.setAttribute('aria-activedescendant',optionId);
}else{
// Remove the attribute instead of setting it to ""
input.removeAttribute('aria-activedescendant');
}
}
</script>
In summary, always ensure aria-activedescendant either points to a real, non-empty id or is removed from the element. Never set it to an empty string.
The aria-controls attribute establishes a programmatic relationship between an interactive element (like a button or link) and the content it controls. Assistive technologies such as screen readers use this relationship to help users navigate between a control and the region it affects—for example, a button that toggles a panel or a tab that switches visible content. When the attribute is present but empty (aria-controls=""or aria-controls=" "), it signals a broken relationship: the browser and assistive technology expect a target element but find nothing.
This issue commonly occurs when aria-controls is added by a JavaScript framework or template before the target element's id is known, leaving behind an empty placeholder. It can also happen when a CMS or component library outputs the attribute unconditionally, even when no controlled region exists.
Why this matters
- Accessibility: Screen readers may announce that a control is associated with another element, but an empty reference leads nowhere. This creates a confusing or misleading experience for users who rely on assistive technology.
- Standards compliance: The HTML specification defines
aria-controlsas an IDREFS attribute, meaning its value must contain one or more space-separated tokens, each matching theidof an element in the same document. An empty value is invalid per both the WAI-ARIA specification and the HTML standard. - Maintainability: Empty or placeholder ARIA attributes are a sign of incomplete implementation. They can mask bugs in dynamic UIs where the controlled element was supposed to be rendered but wasn't, or where an
idwas accidentally removed during refactoring.
How to fix it
- If the element controls another region, set
aria-controlsto theidof that region. Make sure the target element exists in the DOM and has a uniqueid. - If the element doesn't control anything, remove the
aria-controlsattribute entirely. An absent attribute is always better than an empty one. - For dynamically rendered content, only add
aria-controlsafter the target element and itsidare present in the DOM. If using a framework, conditionally render the attribute. - Keep references in sync. If you rename or remove an
id, update everyaria-controlsthat references it.
Examples
Invalid: empty aria-controls
This triggers the validator error because the attribute value contains no id reference.
<ahref="#"aria-controls="">Toggle details</a>
Invalid: whitespace-only value
A value with only spaces is also invalid—IDREFS requires at least one non-whitespace token.
<buttontype="button"aria-controls="">Open menu</button>
Fixed: provide a valid id reference
Point aria-controls to the id of the element being controlled.
<buttontype="button"aria-controls="details-panel"aria-expanded="false">
Toggle details
</button>
<divid="details-panel"hidden>
<p>Here are some additional details.</p>
</div>
Fixed: controlling multiple regions
You can reference multiple id values separated by spaces. Each id must correspond to an element in the document.
<buttontype="button"aria-controls="filters results">
Show filters and results
</button>
<sectionid="filters"hidden>
<p>Filter options...</p>
</section>
<sectionid="results"hidden>
<p>Search results...</p>
</section>
Fixed: remove the attribute when not needed
If the element doesn't actually control another region, simply omit aria-controls.
<ahref="/details">View details</a>
Complete document with proper toggle behavior
This example shows a working toggle pattern combining aria-controls with aria-expanded for full accessibility.
<!doctype html>
<htmllang="en">
<head>
<metacharset="utf-8">
<title>aria-controls Toggle Example</title>
</head>
<body>
<buttontype="button"aria-controls="info-panel"aria-expanded="false">
Toggle info
</button>
<divid="info-panel"hidden>
<p>Extra information goes here.</p>
</div>
<script>
constbtn=document.querySelector('button');
constpanel=document.getElementById('info-panel');
btn.addEventListener('click',()=>{
constexpanded=btn.getAttribute('aria-expanded')==='true';
btn.setAttribute('aria-expanded',String(!expanded));
panel.hidden=expanded;
});
</script>
</body>
</html>
Conditional rendering in a framework
When using a templating system or JavaScript framework, only render aria-controls when the target id is available. Here's a conceptual example:
<!-- Good: only add the attribute when targetId has a value -->
<buttontype="button"aria-controls="sidebar-nav"aria-expanded="false">
Menu
</button>
<navid="sidebar-nav"hidden>
<ul>
<li><ahref="/">Home</a></li>
</ul>
</nav>
<!-- Bad: template outputs an empty value when targetId is undefined -->
<!-- <button type="button" aria-controls="">Menu</button> -->
In frameworks like React, you can conditionally spread the attribute: use aria-controls={targetId || undefined} so that when the value is empty, the attribute is omitted from the rendered HTML entirely rather than being set to an empty string.
The aria-describedby attribute cannot be an empty string — it must either contain valid ID references or be removed entirely.
The aria-describedby attribute accepts one or more ID values (separated by spaces) that point to elements providing additional descriptive text for the current element. When a screen reader focuses on the element, it reads the content of the referenced elements to give the user more context.
Setting aria-describedby="" is invalid because the attribute expects at least one valid IDREF — a non-empty string that matches the id of another element in the page. An empty value doesn't reference anything and creates a validation error. If no description is needed, simply omit the attribute altogether.
This commonly happens when a template or JavaScript dynamically sets the attribute but provides an empty fallback value instead of removing the attribute entirely.
Invalid Example
<labelfor="email">Email</label>
<inputtype="email"id="email"aria-describedby="">
Fixed Examples
If there is no description to reference, remove the attribute:
<labelfor="email">Email</label>
<inputtype="email"id="email">
If a description exists, point to its id:
<labelfor="email">Email</label>
<inputtype="email"id="email"aria-describedby="email-hint">
<pid="email-hint">We'll never share your email with anyone.</p>
If you're generating the attribute dynamically, make sure your code removes aria-describedby entirely rather than setting it to an empty string when no hint is available.
The aria-labelledby attribute accepts an IDREFS value — a space-separated list of one or more id values that reference other elements in the document. The validator expects each ID in the list to be non-empty and contain at least one non-whitespace character. When the attribute is set to an empty string (aria-labelledby=""), it violates this constraint and triggers the validation error.
This issue commonly arises in templating systems and JavaScript frameworks where a variable intended to hold an ID reference resolves to an empty string. For example, a template like aria-labelledby="{{ labelId }}" will produce an empty attribute if labelId is undefined or blank.
Why this matters
The aria-labelledby attribute is one of the highest-priority methods for computing an element's accessible name. According to the accessible name computation algorithm, aria-labelledby overrides all other naming sources — including visible text content, aria-label, and the title attribute. When aria-labelledby is present but empty or broken, screen readers may calculate the link's accessible name as empty, effectively making the link invisible or meaningless to assistive technology users. A link with no accessible name is a significant accessibility barrier: users cannot determine where the link goes or what it does.
Beyond accessibility, an empty aria-labelledby also signals invalid HTML according to both the WHATWG HTML living standard and the WAI-ARIA specification, which define the IDREFS type as requiring at least one valid token.
How to fix it
You have several options depending on your situation:
- Reference a valid ID — Point
aria-labelledbyto theidof an existing element whose text content should serve as the link's accessible name. - Remove the attribute and use visible link text — If the link already contains descriptive text,
aria-labelledbyis unnecessary. - Use
aria-labelinstead — For icon-only links where no visible label element exists,aria-labelprovides a concise accessible name directly on the element. - Conditionally render the attribute — In templates, use conditional logic to omit
aria-labelledbyentirely when there's no valid ID to reference, rather than rendering an empty value.
Examples
Invalid: empty aria-labelledby
This triggers the validation error because the attribute value contains no non-whitespace characters.
<ahref="/report"aria-labelledby=""></a>
Invalid: whitespace-only aria-labelledby
A value containing only spaces is equally invalid — IDREFS requires at least one actual token.
<ahref="/report"aria-labelledby=""></a>
Fixed: referencing an existing element by id
The aria-labelledby attribute points to a <span> whose text content becomes the link's accessible name.
<ahref="/report"aria-labelledby="report-link-text">
<svgaria-hidden="true"viewBox="0 0 16 16"></svg>
</a>
<spanid="report-link-text">View report</span>
Fixed: referencing multiple IDs
You can concatenate text from multiple elements by listing their IDs separated by spaces. The accessible name is built by joining the referenced text in order.
<spanid="action">Learn more:</span>
<spanid="subject">Apples</span>
<ahref="/apples"aria-labelledby="action subject">
<svgaria-hidden="true"viewBox="0 0 16 16"></svg>
</a>
In this case, the computed accessible name is "Learn more: Apples".
Fixed: using visible link text instead
When the link already contains descriptive text, no ARIA attribute is needed. This is the simplest and most robust approach.
<ahref="/report">View report</a>
Fixed: using aria-label for an icon-only link
When there is no separate visible label element to reference, aria-label provides the accessible name directly.
<ahref="/search"aria-label="Search">
<svgaria-hidden="true"viewBox="0 0 16 16"></svg>
</a>
Fixed: conditional rendering in a template
If you're using a templating engine, conditionally include the attribute only when a value exists. The exact syntax varies by framework, but here's the general idea:
<!-- Instead of always rendering the attribute: -->
<!-- <a href="/report" aria-labelledby="{{ labelId }}"> -->
<!-- Only render it when labelId has a value: -->
<!-- <a href="/report" {{#if labelId}}aria-labelledby="{{labelId}}"{{/if}}> -->
This prevents the empty-attribute problem at its source rather than patching it after the fact.
The aria-labelledby attribute is an IDREFS attribute, meaning its value must be a space-separated list of one or more id values that exist in the document. These referenced elements collectively provide the accessible name for the element. When the value is an empty string ("") or contains only whitespace, there are no valid ID references, which violates the IDREFS requirement defined in the WAI-ARIA and HTML specifications.
This issue commonly appears when templating systems or JavaScript frameworks conditionally set aria-labelledby but output an empty string when no label ID is available. It also occurs when developers add the attribute as a placeholder with the intention of filling it in later but forget to do so.
Why this matters
An empty aria-labelledby is problematic for several reasons:
- Accessibility: Screen readers rely on
aria-labelledbyto announce the accessible name of an element. An empty value can cause unpredictable behavior — some screen readers may ignore the SVG entirely, while others may fall back to reading unhelpful content or nothing at all. This leaves users who depend on assistive technology without a meaningful description of the graphic. - Standards compliance: The W3C validator flags this as an error because the HTML specification requires IDREFS attributes to contain at least one non-whitespace character. Shipping invalid HTML can signal broader quality issues and may cause problems in strict parsing environments.
- Maintainability: An empty
aria-labelledbyis ambiguous. It's unclear whether the developer intended the SVG to be decorative, forgot to add a reference, or encountered a bug in their templating logic.
How to fix it
Choose the approach that matches your intent:
- Reference a labeling element by ID: If the SVG conveys meaning, add a
<title>element (or another visible text element) inside or near the SVG with a uniqueid, then setaria-labelledbyto thatid. IDs are case-sensitive, so ensure an exact match. - Use
aria-labelinstead: If you want to provide an accessible name directly as a text string without needing a separate element, replacearia-labelledbywitharia-label. - Remove the attribute: If the SVG already has an accessible name through other means (such as visible adjacent text or a
<title>child that doesn't need explicit referencing), simply remove the emptyaria-labelledby. - Mark as decorative: If the SVG is purely decorative and adds no information, remove
aria-labelledbyand addaria-hidden="true"so assistive technology skips it entirely.
When generating aria-labelledby dynamically, ensure your code omits the attribute entirely rather than outputting an empty value when no label ID is available.
Examples
❌ Empty aria-labelledby (triggers the error)
<svgrole="img"aria-labelledby="">
<usehref="#icon-star"></use>
</svg>
The empty string is not a valid IDREFS value, so the validator reports an error.
✅ Reference a <title> element by ID
<svgrole="img"aria-labelledby="star-title">
<titleid="star-title">Favorite</title>
<usehref="#icon-star"></use>
</svg>
The aria-labelledby points to the <title> element's id, giving the SVG a clear accessible name of "Favorite."
✅ Use aria-label with a text string
<svgrole="img"aria-label="Favorite">
<usehref="#icon-star"></use>
</svg>
When you don't need to reference another element, aria-label provides the accessible name directly as an attribute value.
✅ Reference multiple labeling elements
<svgrole="img"aria-labelledby="star-title star-desc">
<titleid="star-title">Favorite</title>
<descid="star-desc">A five-pointed star icon</desc>
<usehref="#icon-star"></use>
</svg>
The aria-labelledby value can include multiple space-separated IDs. The accessible name is constructed by concatenating the text content of the referenced elements in order.
✅ Decorative SVG (no accessible name needed)
<svgaria-hidden="true"focusable="false">
<usehref="#icon-decorative-divider"></use>
</svg>
For purely decorative graphics, aria-hidden="true" removes the element from the accessibility tree. Adding focusable="false" prevents the SVG from receiving keyboard focus in older versions of Internet Explorer and Edge.
The aria-valuenow attribute requires a valid numeric value and cannot be an empty string.
The aria-valuenow attribute is used on range-type widgets like progress bars, sliders, and spinbuttons to indicate the current value. It must be a valid floating point number — for example, 0, 50, or 3.5. An empty string ("") is not a number, so the validator rejects it.
If the current value is unknown or indeterminate, you should remove the aria-valuenow attribute entirely rather than setting it to an empty string. This is common with indeterminate progress bars where the completion percentage isn't known yet.
When present, aria-valuenow should fall between aria-valuemin and aria-valuemax. All three attributes work together to communicate the state of a range widget to assistive technologies.
Bad Example
<divrole="progressbar"
aria-valuenow=""
aria-valuemin="0"
aria-valuemax="100">
Loading...
</div>
Good Example — Known Value
<divrole="progressbar"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100">
50% complete
</div>
Good Example — Indeterminate State
<divrole="progressbar"
aria-valuemin="0"
aria-valuemax="100">
Loading...
</div>
In the indeterminate example, omitting aria-valuenow tells assistive technologies that the progress is ongoing but the exact value is not known.
The WAI-ARIA specification defines a specific set of role values that assistive technologies like screen readers understand. These include roles such as button, checkbox, alert, dialog, img, navigation, banner, and many others. The value "icon" is not among them. When a browser or assistive technology encounters an unrecognized role, it cannot determine the element's purpose, which defeats the goal of using ARIA in the first place.
This is primarily an accessibility problem. Screen readers rely on valid ARIA roles to communicate the nature of elements to users. An invalid role like "icon" is either ignored or causes unpredictable behavior, leaving users of assistive technologies without the context they need. It's also a standards compliance issue — the W3C validator flags this because the HTML specification requires role values to match roles defined in the ARIA specification.
The fix depends on the purpose of the element:
- Decorative icons (that don't convey information): Remove the
roleattribute entirely, or usearia-hidden="true"to explicitly hide the element from the accessibility tree. - Meaningful icons (that convey information visually): Use
role="img"along with anaria-labelto provide a text alternative. - Icons inside interactive elements: Hide the icon with
aria-hidden="true"and ensure the parent interactive element has an accessible name through visible text or anaria-label.
Examples
❌ Invalid: Using the non-existent "icon" role
<spanclass="icon"role="icon"></span>
This triggers the validation error because "icon" is not a valid ARIA role.
✅ Fixed: Decorative icon with no role
If the icon is purely decorative and doesn't convey any meaning (e.g., it's next to text that already describes the action), simply remove the role attribute. Adding aria-hidden="true" ensures screen readers skip over it completely.
<spanclass="icon"aria-hidden="true"></span>
✅ Fixed: Meaningful icon using role="img"
If the icon conveys meaningful information that isn't available through surrounding text, use role="img" and provide a descriptive aria-label:
<spanclass="icon-warning"role="img"aria-label="Warning"></span>
This tells assistive technologies that the element represents an image and gives it an accessible name of "Warning."
✅ Fixed: Icon inside a button
When an icon is placed inside an interactive element like a button, hide the icon from the accessibility tree and let the button's text or label provide the meaning:
<button>
<spanclass="icon-save"aria-hidden="true"></span>
Save
</button>
If the button has no visible text (an icon-only button), provide an aria-label on the button itself:
<buttonaria-label="Save">
<spanclass="icon-save"aria-hidden="true"></span>
</button>
✅ Fixed: Icon using an <img> element instead
If you're using an actual image file for the icon, consider using a semantic <img> element, which has a built-in img role:
<imgsrc="icon-alert.svg"alt="Alert"class="icon">
For decorative image icons, use an empty alt attribute:
<imgsrc="icon-decorative.svg"alt=""class="icon">
This error occurs when you use role="presentational" on an HTML element. While the intent is clear — you want to strip the element's implicit ARIA semantics — the value presentational does not exist in the WAI-ARIA specification. It's a common typo or misconception. The two valid roles for this purpose are presentation and none, which are synonyms of each other.
When you apply role="presentation" or role="none" to an element, you're telling assistive technologies (like screen readers) to ignore the element's implicit semantic meaning. For example, a <table> used purely for layout purposes has no tabular data semantics, so role="presentation" removes the table-related semantics from the accessibility tree. However, the content inside the element remains accessible — only the container's semantics (and in some cases, its required associated descendants) are removed.
This matters for several reasons:
- Standards compliance: Using an invalid role value means the attribute is essentially ignored by browsers and assistive technologies, so the element's semantics are not removed as intended.
- Accessibility: If a screen reader encounters an unknown role, it may fall back to the element's default semantics, leading to a confusing experience. For instance, a layout table without a valid
presentationrole may still be announced as a data table. - Browser consistency: Invalid ARIA values can lead to unpredictable behavior across different browsers and assistive technologies.
To fix this, simply replace presentational with presentation or none.
Examples
❌ Incorrect: using the invalid presentational role
<tablerole="presentational">
<tr>
<td>Column 1</td>
<td>Column 2</td>
</tr>
</table>
✅ Correct: using presentation
<tablerole="presentation">
<tr>
<td>Column 1</td>
<td>Column 2</td>
</tr>
</table>
✅ Correct: using none (synonym for presentation)
<tablerole="none">
<tr>
<td>Column 1</td>
<td>Column 2</td>
</tr>
</table>
Other elements where this applies
The presentation / none role can be used on various elements, not just tables. Here's another common example with an image used purely for decoration:
<!-- ❌ Incorrect -->
<imgsrc="divider.png"alt=""role="presentational">
<!-- ✅ Correct -->
<imgsrc="divider.png"alt=""role="presentation">
When to use presentation vs. none
Both presentation and none are functionally identical. The none role was introduced later as a clearer name, since presentation can be confused with visual presentation. For maximum browser support, some developers use both:
<tablerole="none presentation">
<tr>
<td>Layout content</td>
</tr>
</table>
This fallback pattern ensures that older assistive technologies that don't recognize none will still pick up presentation. However, in most modern environments, either value alone is sufficient.
The search ARIA role is a landmark role, which means it identifies a large, navigable section of a page — specifically, the region that contains the search functionality. Landmark roles help assistive technologies (like screen readers) quickly identify and jump to major sections of a document. Because landmarks describe sections of a page, they belong on container elements that encompass all the parts of the search interface (the label, the input field, the submit button, etc.), not on a single <input> element.
When you place role="search" on an <input>, the validator rejects it because the search role doesn't match the semantics of an input control. An <input> represents a single interactive widget, not a page region. The valid way to indicate that an input field is for search queries is to use <input type="search">, which gives browsers and assistive technologies the correct semantic meaning for that specific control.
Meanwhile, if you want to mark an entire search form as a search landmark, apply role="search" to the <form> element that wraps the search controls. In modern HTML, you can also use the <search> element, which has the implicit search landmark role without needing any ARIA attribute.
How to fix it
- Remove
role="search"from the<input>element. - Change the input's
typeto"search"— this tells browsers and assistive technologies that the field is for search queries. - Apply
role="search"to the wrapping<form>, or use the HTML<search>element as the container.
Examples
❌ Incorrect: role="search" on an <input>
<form>
<labelfor="query">Search</label>
<inputrole="search"id="query"name="q">
<buttontype="submit">Go</button>
</form>
This triggers the validation error because search is not a valid role for <input>.
✅ Correct: type="search" on the input, role="search" on the form
<formrole="search">
<labelfor="query">Search this site</label>
<inputtype="search"id="query"name="q">
<buttontype="submit">Go</button>
</form>
Here, role="search" is correctly placed on the <form> element, creating a search landmark. The <input type="search"> conveys the correct semantics for the input field itself.
✅ Correct: Using the <search> element (modern HTML)
<search>
<form>
<labelfor="query">Search this site</label>
<inputtype="search"id="query"name="q">
<buttontype="submit">Go</button>
</form>
</search>
The <search> element has the implicit ARIA role of search, so no explicit role attribute is needed on either the container or the form. This is the most semantic approach in browsers that support it.
✅ Correct: Standalone search input without a landmark
If you simply need a search-styled input without marking up a full landmark region, just use type="search":
<labelfor="filter">Filter results</label>
<inputtype="search"id="filter"name="filter">
This gives the input the correct semantics and allows browsers to provide search-specific UI features (such as a clear button) without requiring a landmark role.
The value section does not exist in the WAI-ARIA specification. ARIA defines a specific set of role values, and section is not among them. This is likely a confusion between the HTML element name <section> and the ARIA role region, which is the role that the <section> element implicitly maps to. Because section is not a recognized role, the validator rejects it as an invalid value.
This matters for several reasons. First, assistive technologies like screen readers rely on ARIA roles to communicate the purpose of elements to users. An unrecognized role value may be ignored entirely or cause unexpected behavior, degrading the experience for users who depend on these tools. Second, the <section> element already carries native semantics equivalent to role="region" (when it has an accessible name), so adding a redundant or incorrect role provides no benefit and introduces potential problems.
According to the ARIA in HTML specification, you should generally avoid setting a role on elements that already have appropriate native semantics. The <section> element's implicit role is region, so explicitly adding role="region" is redundant in most cases. The simplest and best fix is to remove the role attribute altogether and let the native HTML semantics do their job.
If you do need to override an element's role for a specific design pattern (for example, turning a <section> into a navigation landmark), use a valid ARIA role from the WAI-ARIA specification.
Examples
Incorrect: using the invalid section role
<sectionrole="section">
<h2>About Us</h2>
<p>Learn more about our team.</p>
</section>
This triggers the validation error because section is not a valid ARIA role value.
Correct: remove the role attribute
<section>
<h2>About Us</h2>
<p>Learn more about our team.</p>
</section>
The <section> element already provides the correct semantics. No role attribute is needed.
Correct: use a valid ARIA role if needed
If you have a specific reason to assign a role, use a valid one. For example, if a <section> is being used as a navigation landmark:
<sectionrole="navigation"aria-label="Main navigation">
<ul>
<li><ahref="/">Home</a></li>
<li><ahref="/about">About</a></li>
</ul>
</section>
In practice, you would typically use a <nav> element instead, which has the navigation role natively. This example simply illustrates that if you do apply a role, it must be a valid ARIA role value.
Correct: explicit region role with an accessible name
If you want to explicitly mark a section as a named region landmark, you can use role="region" along with an accessible name. However, this is redundant when using <section> with an aria-label or aria-labelledby, since the browser already maps it to region:
<!-- Preferred: native semantics handle the role -->
<sectionaria-labelledby="features-heading">
<h2id="features-heading">Features</h2>
<p>Explore our product features.</p>
</section>
<!-- Also valid but redundant -->
<sectionrole="region"aria-labelledby="features-heading">
<h2id="features-heading">Features</h2>
<p>Explore our product features.</p>
</section>
Both are valid HTML, but the first approach is cleaner and follows the principle of relying on native semantics whenever possible.
Validate at scale.
Ship accessible websites, faster.
Automated HTML & accessibility validation for large sites. Check thousands of pages against WCAG guidelines and W3C standards in minutes, not days.
Pro Trial
Full Pro access. Cancel anytime.
Start Pro Trial →Join teams across 40+ countries