Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-use-anchor-content-image.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#9210](https://github.com/biomejs/biome/issues/9210): [`useAnchorContent`](https://biomejs.dev/linter/rules/use-anchor-content/) no longer reports an accessibility error for Astro `Image` components inside links when they provide non-empty `alt` text.
14 changes: 10 additions & 4 deletions crates/biome_html_analyze/src/lint/a11y/use_anchor_content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ impl Rule for UseAnchorContent {
}

// Check if the anchor has accessible content
if has_accessible_content(&html_element.children()) {
let is_astro = source_type.is_astro();
if has_accessible_content(&html_element.children(), is_astro) {
return None;
}

Expand Down Expand Up @@ -190,14 +191,14 @@ impl Rule for UseAnchorContent {
}

/// Checks if `HtmlElementList` contains accessible content (non-empty text or visible elements).
fn has_accessible_content(html_child_list: &HtmlElementList) -> bool {
fn has_accessible_content(html_child_list: &HtmlElementList, is_astro: bool) -> bool {
html_child_list.into_iter().any(|child| match &child {
AnyHtmlElement::AnyHtmlContent(content) => is_accessible_text_content(content),
AnyHtmlElement::HtmlElement(element) => {
if html_element_has_truthy_aria_hidden(element) {
false
} else {
has_accessible_content(&element.children())
has_accessible_content(&element.children(), is_astro)
}
}
AnyHtmlElement::HtmlSelfClosingElement(element) => {
Expand All @@ -212,7 +213,10 @@ fn has_accessible_content(html_child_list: &HtmlElementList) -> bool {
let tag_text = element.name().ok().and_then(|n| n.token_text_trimmed());

match tag_text.as_ref().map(|t| t.as_ref()) {
Some(name) if name.eq_ignore_ascii_case("img") => {
Some(name)
if name.eq_ignore_ascii_case("img")
|| (is_astro && name == "Image") =>
{
html_self_closing_element_has_non_empty_attribute(element, "alt")
}
Some(name)
Expand All @@ -235,6 +239,8 @@ fn has_accessible_content(html_child_list: &HtmlElementList) -> bool {
});
!is_hidden
}
// Custom components (PascalCase) may render accessible content
Some(name) if name.starts_with(|c: char| c.is_uppercase()) => true,
_ => false,
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
<!-- Image with empty alt attribute -->
<a><img alt="" /></a>

<!-- Image component without alt attribute -->
<a><Image /></a>

<!-- Image component with empty alt attribute -->
<a><Image alt="" /></a>

<!-- Void elements that never provide accessible content -->
<a><br /></a>
<a><hr /></a>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
assertion_line: 83
expression: invalid.astro
---
# Input
Expand All @@ -22,6 +23,12 @@ expression: invalid.astro
<!-- Image with empty alt attribute -->
<a><img alt="" /></a>

<!-- Image component without alt attribute -->
<a><Image /></a>

<!-- Image component with empty alt attribute -->
<a><Image alt="" /></a>

<!-- Void elements that never provide accessible content -->
<a><br /></a>
<a><hr /></a>
Expand Down Expand Up @@ -181,7 +188,7 @@ invalid.astro:17:1 lint/a11y/useAnchorContent ━━━━━━━━━━━
> 17 │ <a><img alt="" /></a>
│ ^^^^^^^^^^^^^^^^^^^^^
18 │
19 │ <!-- Void elements that never provide accessible content -->
19 │ <!-- Image component without alt attribute -->

i All links on a page should have content that is accessible to screen readers.

Expand All @@ -199,11 +206,55 @@ invalid.astro:20:1 lint/a11y/useAnchorContent ━━━━━━━━━━━

× Provide screen reader accessible content when using a elements.

19 │ <!-- Void elements that never provide accessible content -->
> 20 │ <a><br /></a>
19 │ <!-- Image component without alt attribute -->
> 20 │ <a><Image /></a>
│ ^^^^^^^^^^^^^^^^
21 │
22 │ <!-- Image component with empty alt attribute -->

i All links on a page should have content that is accessible to screen readers.

i Accessible content refers to digital content that is designed and structured in a way that makes it easy for people with disabilities to access, understand, and interact with using assistive technologies.

i Follow these links for more information,
WCAG 2.4.4
WCAG 4.1.2


```

```
invalid.astro:23:1 lint/a11y/useAnchorContent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide screen reader accessible content when using a elements.

22 │ <!-- Image component with empty alt attribute -->
> 23 │ <a><Image alt="" /></a>
│ ^^^^^^^^^^^^^^^^^^^^^^^
24 │
25 │ <!-- Void elements that never provide accessible content -->

i All links on a page should have content that is accessible to screen readers.

i Accessible content refers to digital content that is designed and structured in a way that makes it easy for people with disabilities to access, understand, and interact with using assistive technologies.

i Follow these links for more information,
WCAG 2.4.4
WCAG 4.1.2


```

```
invalid.astro:26:1 lint/a11y/useAnchorContent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide screen reader accessible content when using a elements.

25 │ <!-- Void elements that never provide accessible content -->
> 26 │ <a><br /></a>
│ ^^^^^^^^^^^^^
21 │ <a><hr /></a>
22
27 │ <a><hr /></a>
28

i All links on a page should have content that is accessible to screen readers.

Expand All @@ -217,16 +268,16 @@ invalid.astro:20:1 lint/a11y/useAnchorContent ━━━━━━━━━━━
```

```
invalid.astro:21:1 lint/a11y/useAnchorContent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
invalid.astro:27:1 lint/a11y/useAnchorContent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide screen reader accessible content when using a elements.

19 │ <!-- Void elements that never provide accessible content -->
20 │ <a><br /></a>
> 21 │ <a><hr /></a>
25 │ <!-- Void elements that never provide accessible content -->
26 │ <a><br /></a>
> 27 │ <a><hr /></a>
│ ^^^^^^^^^^^^^
22
23 │ <!-- Hidden input is not accessible -->
28
29 │ <!-- Hidden input is not accessible -->

i All links on a page should have content that is accessible to screen readers.

Expand All @@ -240,14 +291,14 @@ invalid.astro:21:1 lint/a11y/useAnchorContent ━━━━━━━━━━━
```

```
invalid.astro:24:1 lint/a11y/useAnchorContent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
invalid.astro:30:1 lint/a11y/useAnchorContent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide screen reader accessible content when using a elements.

23 │ <!-- Hidden input is not accessible -->
> 24 │ <a><input type="hidden" /></a>
29 │ <!-- Hidden input is not accessible -->
> 30 │ <a><input type="hidden" /></a>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
25
31

i All links on a page should have content that is accessible to screen readers.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<a aria-hidden="false">content</a>
<a><span aria-hidden="false">content</span></a>
<a><img alt="description" /></a>
<a><Image alt="description" /></a>
<a title="Home">Home</a>

<!-- Accessible via aria-label or title alone -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
assertion_line: 83
expression: valid.astro
---
# Input
Expand All @@ -21,6 +22,7 @@ expression: valid.astro
<a aria-hidden="false">content</a>
<a><span aria-hidden="false">content</span></a>
<a><img alt="description" /></a>
<a><Image alt="description" /></a>
<a title="Home">Home</a>

<!-- Accessible via aria-label or title alone -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@
<!-- Accessible via aria-label or title alone -->
<a aria-label="Navigate to dashboard"></a>
<a title="Go to settings page"></a>

<!-- Custom components may render accessible content -->
<a><Image alt="description" /></a>
<a><CustomIcon /></a>
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ expression: valid.svelte
<a aria-label="Navigate to dashboard"></a>
<a title="Go to settings page"></a>

<!-- Custom components may render accessible content -->
<a><Image alt="description" /></a>
<a><CustomIcon /></a>

```
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@
<!-- Accessible via aria-label or title alone -->
<a aria-label="Navigate to dashboard"></a>
<a title="Go to settings page"></a>

<!-- Custom components may render accessible content -->
<a><Image alt="description" /></a>
<a><CustomIcon /></a>
Comment on lines +20 to +22
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the non-Astro fixtures out of valid here.

Line 21 and Line 22 codify that Vue custom components satisfy useAnchorContent, which is broader than the Astro-only fix from #9210. That would lock in false negatives for anchors whose component children render no accessible text.

Suggested adjustment
-  <!-- Custom components may render accessible content -->
-  <a><Image alt="description" /></a>
-  <a><CustomIcon /></a>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/vue/valid.vue`
around lines 20 - 22, The test includes non-Astro Vue component cases (<Image
alt="description" /> and <CustomIcon />) in the `valid` fixture which
incorrectly asserts that arbitrary component children satisfy the
useAnchorContent rule; remove or relocate these Vue component anchor cases from
this `valid.vue` file (lines showing <a><Image ... /></a> and <a><CustomIcon
/></a>) and instead place them in an appropriate Vue-specific fixture or mark
them as invalid/explicit Astro-only tests so `useAnchorContent` only treats
Astro component behavior in this `valid` suite.

Copy link
Member

@ematipico ematipico Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai that's the whole point of the test. Since we don't know if a possible component renders something, we add a test to make sure it doesn't trigger a false positive. The previous commit mistakenly emitted a diagnostic

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ematipico, understood — my mistake! Since the linter has no visibility into what a custom component ultimately renders, flagging <a><CustomComponent /></a> as lacking accessible content would indeed be a false positive. Treating PascalCase components as potentially accessible content is the correct conservative approach here.


✏️ Learnings added
Learnt from: ematipico
Repo: biomejs/biome PR: 9369
File: crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/vue/valid.vue:20-22
Timestamp: 2026-03-08T08:18:28.023Z
Learning: In `crates/biome_html_analyze/src/lint/a11y/use_anchor_content.rs` (biomejs/biome), custom components (PascalCase elements) inside `<a>` tags are intentionally treated as potentially accessible content to avoid false positives. The linter cannot statically determine what a custom component renders, so it must not emit a diagnostic for anchors whose only children are custom components. The `valid` fixture files for non-Astro frameworks (e.g., vue/valid.vue, svelte/valid.svelte) correctly include cases like `<a><Image alt="description" /></a>` and `<a><CustomIcon /></a>` as valid to document this behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: dyc3
Repo: biomejs/biome PR: 8291
File: crates/biome_html_formatter/tests/specs/prettier/vue/html-vue/elastic-header.html:10-10
Timestamp: 2025-12-04T13:29:49.287Z
Learning: Files under `crates/biome_html_formatter/tests/specs/prettier` are test fixtures synced from Prettier and should not receive detailed code quality reviews (e.g., HTTP vs HTTPS, formatting suggestions, etc.). These files are test data meant to validate formatter behavior and should be preserved as-is.

Learnt from: ematipico
Repo: biomejs/biome PR: 8537
File: crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs:167-210
Timestamp: 2025-12-22T09:27:13.161Z
Learning: In crates/biome_analyze/**/*analyze/src/**/*.rs, the `fix_kind` field in `declare_lint_rule!` should only be specified when the rule implements the `action` function. Rules that only emit diagnostics without providing code fixes should not include `fix_kind` in their metadata.

</template>
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ expression: valid.vue
<!-- Accessible via aria-label or title alone -->
<a aria-label="Navigate to dashboard"></a>
<a title="Go to settings page"></a>

<!-- Custom components may render accessible content -->
<a><Image alt="description" /></a>
<a><CustomIcon /></a>
</template>

```