fix(image-alt): allow whitespace alt on presentational images - #5218
Conversation
Whitespace-only alt values were always failing image-alt via the alt-space-value none check, even when role=presentation/none made the image decorative. Skip that check when the resolved role is presentation or none. Closes #5216 Co-authored-by: Cursor <[email protected]>
There was a problem hiding this comment.
From Claude: Reviewed the fix. Clean, well-scoped change. Using getRole (rather than a raw role read) is the right call—conflict-resolution cases (focusable/globally-labelled presentational images) still fall back to the implicit img role and continue to fail correctly. Good coverage across unit, integration, and virtual-rules tests. No blocking concerns.
There was a problem hiding this comment.
Pull request overview
Fixes an image-alt false positive where <img alt=" " role="presentation|none"> was incorrectly treated as failing due to the alt-space-value check, by exempting presentational images (while still failing conflict-resolution cases via resolved role logic).
Changes:
- Update
alt-space-valueevaluation to ignore whitespace-onlyaltwhen the resolved role ispresentationornone. - Add unit coverage for presentational/none role behavior and conflict-resolution behavior in
alt-space-value. - Add integration + virtual-rule pass cases for
image-altwith whitespace-onlyaltand presentational roles.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/integration/virtual-rules/image-alt.js | Adds virtual-rule pass cases for whitespace-only alt when role is presentational. |
| test/integration/rules/image-alt/image-alt.json | Extends the integration “passes” list to include the new presentational whitespace-alt cases. |
| test/integration/rules/image-alt/image-alt.html | Adds fixtures for `<img alt=" " role="presentation |
| test/checks/label/alt-space-value.js | Adds unit tests ensuring presentational/none roles don’t trigger the whitespace-alt failure, while conflict-resolution still does. |
| lib/checks/label/alt-space-value-evaluate.js | Implements role-aware exemption for whitespace-only alt on presentational images. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function altSpaceValueEvaluate(node, options, virtualNode) { | ||
| if (['presentation', 'none'].includes(getRole(virtualNode))) { | ||
| return false; | ||
| } | ||
|
|
There was a problem hiding this comment.
The sounds valid. Check it out?
There was a problem hiding this comment.
This shouldn't cause a noticeable performance problem. If we do find this is a problem I think the correct answer is to memoize getRole rather than trying to micro optimize where the function is called.
chutchins25
left a comment
There was a problem hiding this comment.
Nice work — clean, well-scoped fix.
Verified the key points:
alt-space-valueis used only inimage-alt'snonearray, so the exemption can't leak into other rules.getRoleapplies conflict resolution, so focusable/globally-labelled "presentational" images resolve back to the implicitimgrole and still fail correctly — the addedtabindex="0"unit test proves it.imgisn't in the presentation-inheritance chain, so there's nogetInheritedRoleReferenceError risk here.
Coverage across unit, integration, and virtual-rules is good.
One optional, non-blocking note: our convention is to include an open Shadow DOM unit case for check changes. This particular branch isn't DOM-boundary sensitive for img, and the integration + virtual-rules cases already cover the behavior, so I don't consider it a blocker — flagging only for completeness.
(No comment on the getRole hot-path ordering — Copilot's inline suggestion already covers that and it's a reasonable micro-optimization.)
Switching my Approve to a Comment so the getRole hot-path suggestion isn't lost — see updated review.
chutchins25
left a comment
There was a problem hiding this comment.
Clean, well-scoped fix. Verified the key points:
alt-space-valueis used only inimage-alt'snonearray, so the exemption can't leak into other rules.getRoleapplies conflict resolution, so focusable/globally-labelled "presentational" images resolve back to the implicitimgrole and still fail correctly — the addedtabindex="0"unit test proves it.imgisn't in the presentation-inheritance chain, so there's nogetInheritedRoleReferenceError risk here.
Coverage across unit, integration, and virtual-rules is good.
Leaving this as a Comment rather than an Approve on one point: Copilot's inline suggestion to check alt before calling getRole is valid — getRole runs for every <img> and isn't cached, so gating it behind the whitespace check avoids the work in the common case. Worth applying (or explicitly deciding to skip) before merge.
Optional, non-blocking: our convention is to include an open Shadow DOM unit case for check changes. This branch isn't DOM-boundary sensitive for img and is already covered by integration + virtual-rules, so not a blocker — flagging only for completeness.
| function altSpaceValueEvaluate(node, options, virtualNode) { | ||
| if (['presentation', 'none'].includes(getRole(virtualNode))) { | ||
| return false; | ||
| } | ||
|
|
There was a problem hiding this comment.
This shouldn't cause a noticeable performance problem. If we do find this is a problem I think the correct answer is to memoize getRole rather than trying to micro optimize where the function is called.
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 🤖 🎉
Summary
image-altfor<img alt=" " role="presentation|none">by skipping thealt-space-valuecheck when the resolved role is presentationalgetRolearia-allowed-roleas-is — whitespace alt on a presentational image remains a minor best-practice flagCloses #5216
Test plan
alt-space-value(presentation/none pass; conflict resolution still fails)image-alt(#pass12,#pass13)<img alt=" " role="presentation">passesimage-altand still failsaria-allowed-roleMade with Cursor