Skip to content

feat: add template-anchor-has-content#2757

Draft
johanrd wants to merge 2 commits intoember-cli:masterfrom
johanrd:feat/template-anchor-has-content
Draft

feat: add template-anchor-has-content#2757
johanrd wants to merge 2 commits intoember-cli:masterfrom
johanrd:feat/template-anchor-has-content

Conversation

@johanrd
Copy link
Copy Markdown
Contributor

@johanrd johanrd commented Apr 27, 2026

Note

This is part of a series where Claude has audited eslint-plugin-ember against jsx-a11y, vuejs-accessibility, angular-eslint, lit-a11y and html-validate, ember-template-lint, and the HTML and WCAG specs.

Every <a href> rendered to the DOM is exposed as a link to assistive tech. Per ACCNAME 1.2 §4.3.2, the accessible name is computed from: aria-labelledbyaria-label → descendant text content → title. An anchor with none of these has no accessible name; a screen reader announces "link" with nothing else. Authoring basis: WCAG 2.1 SC 2.4.4 Link Purpose.

Adds template-anchor-has-content: flags <a href> with no text, no accessible-name attribute (aria-label / aria-labelledby / title), and no child that contributes an accessible name. Dynamic cases (mustache-only content) stay accepted — we can't know at lint time whether they resolve to a non-empty name.

aria-hidden treatment

The question "what does <span aria-hidden> (bare), aria-hidden="" (empty), or aria-hidden={{false}} mean?" has no single authoritative answer. We lean toward fewer false positives: treat a child with valueless / empty aria-hidden as NOT hidden — if someone writes <a href="/x"><span aria-hidden>X</span></a>, the child's content likely still contributes a name, so we don't flag the anchor. Only explicit aria-hidden="true" / {{true}} hides the child subtree from the name computation.

Flags

<a href="/x" />
<a href="/x"><span aria-hidden="true">X</span></a>
<a href="/x"><img aria-hidden="true" alt="Nope" /></a>
<a href="/x"><img /></a>
<a href="/x" aria-label="" />
<a href="/x">   </a>

Allows

<a href="/x">link text</a>
<a href="/x"><span aria-hidden>X</span></a>        {{! valueless — child content counts }}
<a href="/x"><img aria-hidden alt="Nope" /></a>    {{! same }}
<a href="/x" aria-label="Close" />
<a href="/x">{{@label}}</a>
<a href="/x"><img alt="Search" /></a>
<Link href="/x" />

Prior art

Plugin Rule Behavior
jsx-a11y anchor-has-content Flags <a> without text content or a recognized accessible-name attribute.
vuejs-accessibility anchor-has-content Flags <a> lacking content and aria-label; configurable accessibleChildren / accessibleDirectives.
lit-a11y No anchor-has-content equivalent (anchor-is-valid covers href validity, not accessible-name content).
@angular-eslint/template No equivalent rule.

@johanrd johanrd marked this pull request as draft April 30, 2026 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant