feat(aria-prohibited-attr): allow many elements to be named and disallow label and body from being named - #5259
Conversation
WilcoFiers
left a comment
There was a problem hiding this comment.
Nice cleanup — replacing the "no role at all means no name" inference with explicit spec data is clearly the right direction, and it fixes a batch of long-standing false positives. I built both the merge-base and this branch and diffed rule output across ~60 markup cases to see exactly what moves. Two things need a decision before merge.
Note: the failing test_node (18) and test_node (20) checks are an unrelated undici/jsdom incompatibility, not caused by this PR.
1. del loses naming prohibition while ins keeps it (blocking)
lib/standards/html-elms.js L215 (compare ins at L566)
del didn't get namingProhibited: true, but ins did:
<del aria-label="hello"></del>
before: violation — aria-label attribute cannot be used on a del with no valid role attribute.
after: PASS
<ins aria-label="hello"></ins>
before: violation
after: violation
Naming is prohibited on both — del maps to deletion and ins to insertion, and both carry prohibitedAttrs in aria-roles.js. Since neither element appears in implicitHtmlRoles, the role spec is no longer consulted, so del silently drops its prohibition.
Suggestion: add namingProhibited: true to del, and add a test case pairing del with ins so the two can't drift apart again.
2. body and dd report a role whose spec allows naming (blocking)
lib/checks/aria/aria-prohibited-attr-evaluate.js L76, with body at html-elms.js L135 and dd at L211
These are the only two elements given namingProhibited: true that also have a non-null implicit role, and two problems surface there.
The outcome is right but the reported role is wrong. <body aria-label> should fail — but axe maps body to document and dd to definition, and neither role has prohibitedAttrs, so the message blames a role that permits naming:
<body aria-label="hello"> before: PASS
after: incomplete — aria-label attribute is not well supported with role "document".
<dd aria-label="hello"> before: PASS
after: violation — aria-label attribute cannot be used with role "definition".
I confirmed aria-allowed-attr still passes both, so the two rules now disagree with each other. The real fix is in the mapping: body should be generic and dd should have no corresponding role. Worth noting generic doesn't exist in lib/standards/aria-roles.js at all today, so that correction is a prerequisite rather than a one-liner.
role === implicitRole isn't a safe proxy for "no explicit role." An author can write <dd role="definition"> or <p role="paragraph"> and land in the HTML-spec branch even though an explicit role was set — which is the case the ARIA role spec should govern.
Suggestion: branch on getExplicitRole() rather than comparing the resolved role to the implicit role, and fix the body/dd mappings (adding generic to aria-roles.js) so the prohibition and its message come from the same place. If the mapping fix is too large for this PR, splitting it out would at least keep body and dd from shipping with contradictory messaging in the meantime.
3. Four more elements correctly stop prohibiting names (note)
Beyond del, four elements flip from violation to pass, all correctly:
<address aria-label="hello"> before: violation after: PASS
<blockquote aria-label="hello"> before: violation after: PASS
<hgroup aria-label="hello"> before: violation after: PASS
<ruby aria-label="hello"> before: violation after: PASS
These were false positives from the old catch-all — implicitHtmlRoles only lists elements whose role axe actually needs, so every known HTML element in that gap fell through to return ['aria-label', 'aria-labelledby']. This is a fix, not a regression.
Suggestion: call these out in the PR description so they land in the release notes, and add test cases pinning the new behavior so we don't regress them in the future.
4. doc/check-options.md documents a stale default (non-blocking)
doc/check-options.md L98
The PR changes the elementsAllowedAriaLabel default to ["applet"], but the docs still advertise a twelve-element list including label — precisely what this PR reverses. The table was already stale, but this makes it actively contradict the fix in the PR title. Two-line change while you're in there.
5. noAriaAttrs branch reuses the noRole messages (non-blocking, separate issue)
lib/checks/aria/aria-prohibited-attr-evaluate.js L88
<col aria-invalid="true">
violation — aria-invalid attribute cannot be used on a col with no valid role attribute.
The wording implies a valid role would fix it, but col permits no ARIA attributes at all regardless of role. Same message now reaches map, picture, base, head, html, link, meta, noscript, param, script, slot, source, style, template, title, and track. A dedicated message key would read better — worth its own issue rather than expanding this PR.
| contentTypes: ['phrasing', 'flow'], | ||
| allowedRoles: false, | ||
| noAriaAttrs: true | ||
| allowedAriaAttrs: ['aria-hidden'] |
There was a problem hiding this comment.
I think this needs a test too.
There was a problem hiding this comment.
Turns out we can't test this as aria-prohibited-attr ignores elements that aren't visible to screen readers
| <iframe | ||
| aria-label="value" | ||
| id="pass82" | ||
| srcdoc="<main><h1>hello</h1></main>" |
There was a problem hiding this comment.
Was tired of having this fail whenever I changed the playground code. Now it should always pass
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. ## [4.13.0](v4.12.1...v4.13.0) (2026-08-05) ### Features - **aria-actions:** add aria-actions to allowed ARIA attributes ([#5200](#5200)) ([029655d](029655d)), closes [#4584](#4584) [#5199](#5199), references [#5215](#5215) [#5215](#5215) - **aria-allowed-attr:** flag deprecated ARIA attributes as needs-review ([#5246](#5246)) ([518f3cc](518f3cc)), closes [#3341](#3341) - **aria-prohibited-attr:** allow many elements to be named and disallow label and body from being named ([#5259](#5259)) ([d8b1ea5](d8b1ea5)) - **aria-roles:** add sectionheader and sectionfooter roles ([#5238](#5238)) ([c36c109](c36c109)), closes [#4734](#4734), references [#4734](#4734) - **aria/get-aria-value:** new function to get aria values of a node ([#5109](#5109)) ([a7d8f3e](a7d8f3e)), references [#5042](#5042) - **aria/has-attr-value:** new function to check if node has aria value ([#5136](#5136)) ([61f2624](61f2624)), references [#5109](#5109) - **aria:** support role=image as equivalent to role=img ([#5248](#5248)) ([5aa8aaf](5aa8aaf)), closes [#4656](#4656), references [#5272](#5272) - **checks/aria:** support ARIA element internals properties ([#5172](#5172)) ([9b7f754](9b7f754)) - **checks/label:** support ARIA element internals properties ([#5170](#5170)) ([21c5f8b](21c5f8b)) - **checks/navigation:** support ARIA element internals properties ([#5167](#5167)) ([2c3a98f](2c3a98f)) - **commons/aria:** support ARIA element internals properties ([#5171](#5171)) ([31f09e7](31f09e7)) - **commons/dom:** support ARIA element internals properties ([#5163](#5163)) ([f0a12cf](f0a12cf)) - **commons/forms:** support ARIA element internals properties ([#5165](#5165)) ([27a4686](27a4686)) - **commons/matches/fromPrimative:** deprecate in favor of correct spelling ([#5270](#5270)) ([31cfb2e](31cfb2e)) - **commons/text:** support ARIA element internals properties ([#5169](#5169)) ([e841a33](e841a33)) - **commons/text:** support form-associated labels via element internals ([#5182](#5182)) ([57cfe0a](57cfe0a)), closes [#5045](#5045), references [#5170](#5170) [#5039](#5039) [#5151](#5151) [#5039](#5039) - **dom/getResolvedRefs:** new function to get the resolved virtual nodes of idrefs ([#5151](#5151)) ([489cdea](489cdea)), references [#5109](#5109) - **element-internals:** enable ElementInternals by default ([#5284](#5284)) ([2740d42](2740d42)), closes [#5277](#5277) - **i18n:** Add Swedish locale ([#5190](#5190)) ([dcd13f2](dcd13f2)), references [#5189](#5189) - **matches:** add inSectioningContent, hasChild, and isSummaryForDetails matches ([#5262](#5262)) ([c47cdcd](c47cdcd)) - **rules:** support ARIA element internals properties ([#5168](#5168)) ([065baf7](065baf7)) - **standards/ariaAttrs:** add caseInsensitive property for attributes ([#5224](#5224)) ([bcd791c](bcd791c)) ### Bug Fixes - **aria-allowed-role:** allow roles on a non-details summary ([#5242](#5242)) ([3bd9875](3bd9875)), closes [#3911](#3911), references [#3443](#3443) [#3911](#3911) - **aria-allowed-role:** restrict figure roles with child figcaption ([#5240](#5240)) ([178a635](178a635)), closes [#3443](#3443) - **aria-prohibited-attr:** visible aria-labelledby requires review only ([#5285](#5285)) ([fd6fa9f](fd6fa9f)) - **axe.d.ts:** make enabled property of RuleMetadata optional ([#5129](#5129)) ([90fce18](90fce18)) - **color-contrast:** fix various stacking context bugs ([#5214](#5214)) ([d5e5b04](d5e5b04)), references [#8](#8) [#5213](#5213) - **gather-internals:** handle non-HTMLElement nodes ([#5161](#5161)) ([06e84c3](06e84c3)) - **get-selector:** escape control characters in attribute selectors ([#5273](#5273)) ([4b60ac5](4b60ac5)), closes [#5204](#5204) [#5204](#5204) - **image-alt:** allow whitespace alt on presentational images ([#5218](#5218)) ([c5dd0ef](c5dd0ef)), closes [#5216](#5216) - **landmark-unique:** exclude section/form with non-landmark roles from landmark match ([#5085](#5085)) ([c5fd013](c5fd013)), closes [#4722](#4722) [#5064](#5064) - name the image role in role-img-alt and svg-img-alt metadata ([#5279](#5279)) ([995a269](995a269)), closes [#5272](#5272), references [#5248](#5248) [#5248](#5248) - **standards:** update aria-errormessage and aria-details to be idrefs ([#5157](#5157)) ([fb94f8a](fb94f8a)) This PR was opened by a robot 🤖 🎉
After looking over the HTML in ARIA spec, I realized that we could better handle how we determine when an attribute is prohibited. Basically if the element doesn't have an explicit role, then we use the
htmlElmspec to determine prohibited attrs and naming, otherwise we use theariaRolesspec to determine that. This should allow us to eliminate theelementsAllowedAriaLabeloption that was introduced to fix which elements allowed naming and then modified when we added Chromium roles to fix conflict name resolution. However the option still has theappletelement which isn't in our html spec so I'm leaving it for now.Elements which allow naming now include:
section,blockquote,address,hgroup,ruby.This also adds enforcement for elements that are not allowed any aria-* attribute (which has never been used in our code) by changing the
noAriaAttrsspec property to an emptyallowedAriaAttrsso that they now throw in the rulearia-allowed-attr.Closes: #5185
Closes: #3410