Skip to content

feat(html): port noAriaHiddenOnFocusable a11y rule to HTML#9496

Merged
ematipico merged 6 commits intobiomejs:nextfrom
aviraldua93:feat/html-no-aria-hidden-on-focusable
Apr 6, 2026
Merged

feat(html): port noAriaHiddenOnFocusable a11y rule to HTML#9496
ematipico merged 6 commits intobiomejs:nextfrom
aviraldua93:feat/html-no-aria-hidden-on-focusable

Conversation

@aviraldua93
Copy link
Copy Markdown

@aviraldua93 aviraldua93 commented Mar 16, 2026

Summary

Ports the noAriaHiddenOnFocusable lint rule from JSX to HTML, as part of the umbrella issue #8155.

Enforces that aria-hidden="true" is not set on focusable elements. A focusable element with aria-hidden can be reached by keyboard but is invisible to screen readers, causing confusion.

What counts as focusable:

  • Elements with non-negative tabindex (e.g., tabindex="0")
  • Natively focusable elements: <button>, <input>, <select>, <textarea>, <details>, <summary>, <a href="...">, <area href="...">
  • Elements with contenteditable="true", contenteditable="", or contenteditable="plaintext-only" (editing hosts per HTML spec §6.8.1)
  • <input> except <input type="hidden"> which is not focusable

Exemptions:

  • aria-hidden="true" on non-focusable elements (div, span, etc.) is valid
  • tabindex="-1" with aria-hidden="true" is valid (intentionally removed from tab order)

Fix: Provides an unsafe fix that removes the aria-hidden attribute.

Closes part of #8155.

Test Plan

  • valid.html / invalid.html: native focusable elements, tabindex handling, <input type="hidden">, <a> without href, <area> without href, contenteditable states, custom components (<MyButton>), uppercase tags (<BUTTON>, <INPUT>)
  • vue/valid.vue / vue/invalid.vue: Vue framework coverage with custom components
  • svelte/valid.svelte / svelte/invalid.svelte: Svelte framework coverage with custom components
  • astro/valid.astro / astro/invalid.astro: Astro framework coverage with custom components
  • All 8 tests pass locally, snapshots generated via cargo insta, clippy clean (zero warnings)

AI Disclosure

This PR was developed with assistance from GitHub Copilot (Claude). The implementation, review comment responses, and test cases were reviewed and verified by the author, including running clippy and cargo test locally.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 16, 2026

🦋 Changeset detected

Latest commit: 309f915

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@biomejs/biome Minor
@biomejs/cli-win32-x64 Minor
@biomejs/cli-win32-arm64 Minor
@biomejs/cli-darwin-x64 Minor
@biomejs/cli-darwin-arm64 Minor
@biomejs/cli-linux-x64 Minor
@biomejs/cli-linux-arm64 Minor
@biomejs/cli-linux-x64-musl Minor
@biomejs/cli-linux-arm64-musl Minor
@biomejs/wasm-web Minor
@biomejs/wasm-bundler Minor
@biomejs/wasm-nodejs Minor
@biomejs/backend-jsonrpc Patch
@biomejs/js-api Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added A-Linter Area: linter L-HTML Language: HTML and super languages labels Mar 16, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Introduces a new HTML accessibility lint rule NoAriaHiddenOnFocusable that flags aria-hidden="true" on focusable elements. The rule checks for a truthy aria-hidden attribute, evaluates focusability via tabindex parsing and native focusability rules (interactive tags, a/area with href, input excluding type="hidden", and contenteditable), emits a diagnostic with an explanatory note, and provides an unsafe fixer that removes the offending aria-hidden attribute. New test fixtures cover valid and invalid cases across HTML, Astro, Svelte, and Vue.

Possibly related PRs

Suggested reviewers

  • ematipico
  • dyc3
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: porting the noAriaHiddenOnFocusable accessibility rule to HTML support.
Description check ✅ Passed The PR description clearly relates to porting the noAriaHiddenOnFocusable lint rule from JSX to HTML, with detailed coverage of rule logic, focusability criteria, exemptions, and test plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs`:
- Around line 155-164: The check treating all inputs as natively focusable is
causing false positives for <input type="hidden">; update the conditional that
currently uses name_matches("input") (in the focusability check inside
no_aria_hidden_on_focusable.rs) to exclude inputs whose type attribute equals
"hidden" (case-insensitive). Implement the check inline or add a helper like
is_hidden_input(node) that reads the "type" attribute and returns true for
"hidden", and only treat the element as focusable when name_matches("input") &&
!is_hidden_input(node).
- Around line 171-180: has_contenteditable_true currently returns false when the
contenteditable attribute exists without a value or is an empty string; change
the logic to treat an attribute present with no value or an empty value as
truthy per the HTML spec: call element.find_attribute_by_name("contenteditable")
and match on the Option, returning false only when the attribute is None, but
when Some(attr) return true if attr.value() is None OR (attr.value() is Some(v)
and v.trim().eq_ignore_ascii_case("false") is false); update
has_contenteditable_true (and reference AnyHtmlElement/find_attribute_by_name
and attr.value()) accordingly so missing-value and empty-string cases are
detected as true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7ff8e9ec-a6c9-43ea-b217-637a1aecbdcb

📥 Commits

Reviewing files that changed from the base of the PR and between 8f47f06 and a91f417.

📒 Files selected for processing (3)
  • crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html

Comment thread crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs Outdated
Comment thread crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs Outdated
@aviraldua93 aviraldua93 force-pushed the feat/html-no-aria-hidden-on-focusable branch from a91f417 to b57ecff Compare March 16, 2026 04:27
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs (2)

171-180: ⚠️ Potential issue | 🟠 Major

Treat empty/missing contenteditable value as truthy.

At Line 175-179, has_contenteditable_true misses <div contenteditable ...> and <div contenteditable="" ...>, which should both be treated as editable in HTML. This creates false negatives.

Proposed patch
 fn has_contenteditable_true(element: &AnyHtmlElement) -> bool {
     element
         .find_attribute_by_name("contenteditable")
-        .is_some_and(|attr| {
-            attr.value().is_some_and(|value| {
-                let trimmed = value.trim();
-                !trimmed.is_empty() && !trimmed.eq_ignore_ascii_case("false")
-            })
-        })
+        .is_some_and(|attr| match attr.value() {
+            None => true,
+            Some(value) => {
+                let trimmed = value.trim();
+                trimmed.is_empty() || !trimmed.eq_ignore_ascii_case("false")
+            }
+        })
 }
Per HTML spec, what state does contenteditable have when present without a value, or with an empty string value?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs`
around lines 171 - 180, The function has_contenteditable_true incorrectly treats
elements like <div contenteditable> and <div contenteditable=""> as
non-editable; update has_contenteditable_true to treat presence of the
contenteditable attribute with no value or an empty string as truthy per the
HTML spec by returning true when the attribute exists and its value is either
missing or empty, and only return false when the attribute value is explicitly
"false" (case-insensitive); locate has_contenteditable_true and adjust the
attribute value check to consider attr.value() being None or Some("") as true
while still treating "false" as false.

155-164: ⚠️ Potential issue | 🟠 Major

Exclude hidden inputs from native-focusable detection.

Line 157 currently treats all <input> elements as focusable. That includes type="hidden", which should not trigger this rule, so this can raise false positives. Also add a fixture for <input type="hidden" aria-hidden="true"> to lock this down.

Proposed patch
-        if name_matches("button")
-            || name_matches("input")
-            || name_matches("select")
+        if name_matches("button")
+            || name_matches("select")
             || name_matches("textarea")
             || name_matches("details")
             || name_matches("summary")
         {
             return true;
         }
+
+        if name_matches("input") {
+            let is_hidden_input = element
+                .find_attribute_by_name("type")
+                .and_then(|attr| attr.value())
+                .is_some_and(|value| value.trim().eq_ignore_ascii_case("hidden"));
+            return !is_hidden_input;
+        }
In the HTML Standard, is <input type="hidden"> part of sequential keyboard focus navigation (tab order)?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs`
around lines 155 - 164, The current native-focusable check treats all inputs as
focusable (the name_matches("input") branch) which incorrectly includes <input
type="hidden">; update the logic so the input case explicitly excludes inputs
where the type attribute is "hidden" (e.g., check the element's "type" attribute
or add an is_hidden_input helper and use it in the early-return condition), and
add a fixture/test for <input type="hidden" aria-hidden="true"> to verify it no
longer triggers the rule; ensure you update the same function containing the
name_matches("input") check so hidden inputs are not considered focusable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs`:
- Around line 171-180: The function has_contenteditable_true incorrectly treats
elements like <div contenteditable> and <div contenteditable=""> as
non-editable; update has_contenteditable_true to treat presence of the
contenteditable attribute with no value or an empty string as truthy per the
HTML spec by returning true when the attribute exists and its value is either
missing or empty, and only return false when the attribute value is explicitly
"false" (case-insensitive); locate has_contenteditable_true and adjust the
attribute value check to consider attr.value() being None or Some("") as true
while still treating "false" as false.
- Around line 155-164: The current native-focusable check treats all inputs as
focusable (the name_matches("input") branch) which incorrectly includes <input
type="hidden">; update the logic so the input case explicitly excludes inputs
where the type attribute is "hidden" (e.g., check the element's "type" attribute
or add an is_hidden_input helper and use it in the early-return condition), and
add a fixture/test for <input type="hidden" aria-hidden="true"> to verify it no
longer triggers the rule; ensure you update the same function containing the
name_matches("input") check so hidden inputs are not considered focusable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 08de291f-12a3-42f6-85da-cbf9ffc788ea

📥 Commits

Reviewing files that changed from the base of the PR and between a91f417 and b57ecff.

⛔ Files ignored due to path filters (2)
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (4)
  • .changeset/port-no-aria-hidden-on-focusable-html.md
  • crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html
✅ Files skipped from review due to trivial changes (1)
  • .changeset/port-no-aria-hidden-on-focusable-html.md

@aviraldua93 aviraldua93 force-pushed the feat/html-no-aria-hidden-on-focusable branch from b57ecff to 31fb6dc Compare March 16, 2026 04:43
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/invalid.html`:
- Around line 9-10: The test snapshot for the noAriaHiddenOnFocusable spec is
out of sync because two new invalid inputs were added (contenteditable elements
with aria-hidden); regenerate and commit the updated snapshot for that fixture:
run the snapshot-update command used by this repo (e.g. the insta snapshot
update flow or the project’s test update command) for the
tests/specs/a11y/noAriaHiddenOnFocusable suite, verify the new diagnostics for
the two added lines are present, and commit the updated invalid.html.snap so the
fixture and expected output match.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6d1b4aee-6ec5-4d0a-b339-dda8ee5672bc

📥 Commits

Reviewing files that changed from the base of the PR and between b57ecff and 31fb6dc.

⛔ Files ignored due to path filters (2)
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (5)
  • .changeset/port-no-aria-hidden-on-focusable-html.md
  • crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html
  • pr-body.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • .changeset/port-no-aria-hidden-on-focusable-html.md
  • crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs

Comment on lines +9 to +10
<div contenteditable aria-hidden="true">editable bare attr</div>
<div contenteditable="" aria-hidden="true">editable empty string</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Update the spec snapshot for the new contenteditable cases.

Lines 9–10 add two new invalid inputs, but the existing invalid.html.snap (shown in context) doesn’t include their diagnostics yet. Please regenerate/update the snapshot so the fixture and expected output stay in sync.

🤖 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/noAriaHiddenOnFocusable/invalid.html`
around lines 9 - 10, The test snapshot for the noAriaHiddenOnFocusable spec is
out of sync because two new invalid inputs were added (contenteditable elements
with aria-hidden); regenerate and commit the updated snapshot for that fixture:
run the snapshot-update command used by this repo (e.g. the insta snapshot
update flow or the project’s test update command) for the
tests/specs/a11y/noAriaHiddenOnFocusable suite, verify the new diagnostics for
the two added lines are present, and commit the updated invalid.html.snap so the
fixture and expected output match.

@Netail Netail mentioned this pull request Mar 16, 2026
32 tasks
Copy link
Copy Markdown
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

Thank you, there are a lot of missing tests:

  • case sensitivity
  • components

"@biomejs/biome": minor
---

# Port `noAriaHiddenOnFocusable` a11y lint rule to HTML
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Refer to our contribution guide on how to write a changeset

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — changeset now follows the contribution guide format with a description, HTML example snippet, and bumped to minor.

Comment on lines +79 to +80
if let Some(tabindex_attr) = element.find_attribute_by_name("tabindex") {
if let Some(tabindex_value) = get_tabindex_value(&tabindex_attr) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use let chain. You didn't use clippy, did you ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — tabindex checking now uses a let-chain (lines 77-79). Ran clippy locally, zero warnings.

value.trim().parse::<i32>().ok()
}

/// Checks if an element is natively focusable or has contenteditable.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

docstring should explain the business logic of a function. Here, you want to explain when an element is focusable

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — is_focusable_element docstring now explains exactly when an element is focusable: interactive elements, elements with href, input (except type=hidden), and editing hosts (contenteditable). Returns Some(true)/Some(false)/None semantics documented.

}

/// Checks if an element is natively focusable or has contenteditable.
fn is_focusable_element(element: &AnyHtmlElement, is_html: bool) -> bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Change the return type to Option<bool> so that you can use the try operator a reduce code branching

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — is_focusable_element now returns Option and the caller uses ? directly (line 89).

if is_html {
element_name.eq_ignore_ascii_case(name)
} else {
element_name.text() == name
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You muse use text_trimmed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Investigated this. element.name() returns TokenText which is already constructed from token_text_trimmed() internally (see element_ext.rs:17-28). The .text() call on TokenText returns trivia-free text — TokenText doesn't have a text_trimmed() method (only SyntaxToken does). This matches the pattern used by use_button_type.rs, use_alt_text.rs, use_anchor_content.rs, and use_media_caption.rs. The diagnostic range already uses text_trimmed_range() (line 103). Happy to change if you'd prefer a different approach.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 17, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 168 skipped benchmarks1


Comparing aviraldua93:feat/html-no-aria-hidden-on-focusable (2e26411) with next (cbbb7d5)

Open in CodSpeed

Footnotes

  1. 168 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@aviraldua93 aviraldua93 force-pushed the feat/html-no-aria-hidden-on-focusable branch from 31fb6dc to 1ffe6b4 Compare March 25, 2026 03:41
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html (1)

13-13: Consider adding an invalid contenteditable value test case.

Per the HTML spec, invalid values like contenteditable="banana" fall back to the Inherit state (not an editing host). Adding a case like <div contenteditable="banana" aria-hidden="true"> would document this edge case behaviour.

Suggested addition
 <div contenteditable="false" aria-hidden="true">not editable</div>
+<div contenteditable="banana" aria-hidden="true">invalid value inherits</div>
 <BUTTON aria-hidden="true" tabindex="-1"></BUTTON>
🤖 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/noAriaHiddenOnFocusable/valid.html`
at line 13, Add a test case showing the invalid contenteditable value behavior
by including an element like <div contenteditable="banana"
aria-hidden="true">not editable</div> in the valid.html tests for
noAriaHiddenOnFocusable; this documents that invalid values fall back to Inherit
(not an editing host) so aria-hidden on that element is acceptable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs`:
- Around line 165-173: The natively-focusable check (the block using
name_matches for "button", "select", "textarea", "details", "summary") should
also verify the element is not disabled before returning Some(true); update that
logic (e.g., call element.find_attribute_by_name("disabled").is_none()) so
disabled form controls are treated as unfocusable, and add corresponding tests
(e.g., fixtures with <button disabled aria-hidden="true">) to the valid cases to
cover this behavior for both the Rust rule and the JS equivalent.

---

Nitpick comments:
In
`@crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html`:
- Line 13: Add a test case showing the invalid contenteditable value behavior by
including an element like <div contenteditable="banana" aria-hidden="true">not
editable</div> in the valid.html tests for noAriaHiddenOnFocusable; this
documents that invalid values fall back to Inherit (not an editing host) so
aria-hidden on that element is acceptable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6126c960-eb87-415d-b914-53e65ab076e4

📥 Commits

Reviewing files that changed from the base of the PR and between 31fb6dc and 1ffe6b4.

📒 Files selected for processing (4)
  • .changeset/port-no-aria-hidden-on-focusable-html.md
  • crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html
✅ Files skipped from review due to trivial changes (1)
  • .changeset/port-no-aria-hidden-on-focusable-html.md

Comment on lines +165 to +173
// These elements are always natively focusable
if name_matches("button")
|| name_matches("select")
|| name_matches("textarea")
|| name_matches("details")
|| name_matches("summary")
{
return Some(true);
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 25, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the JSX version handles disabled elements
rg -n -A 10 "disabled" crates/biome_js_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# First, let's find the correct paths for both HTML and JS rules
fd -t f "no_aria_hidden_on_focusable" crates/

Repository: biomejs/biome

Length of output: 257


🏁 Script executed:

# Let's also check the structure of the analyzers
ls -la crates/ | grep analyze

Repository: biomejs/biome

Length of output: 536


🏁 Script executed:

# Search for how disabled is handled in the HTML rule
rg -n "disabled" crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Read the HTML rule implementation
wc -l crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs

Repository: biomejs/biome

Length of output: 131


🏁 Script executed:

# Read the JS rule implementation
wc -l crates/biome_js_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs

Repository: biomejs/biome

Length of output: 129


🏁 Script executed:

# Check test specs for HTML rule
fd -t f "no_aria_hidden_on_focusable" crates/biome_html_analyze/tests/

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Read the HTML rule implementation
cat -n crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs | head -100

Repository: biomejs/biome

Length of output: 4364


🏁 Script executed:

# Read the JS rule implementation
cat -n crates/biome_js_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs

Repository: biomejs/biome

Length of output: 6722


🏁 Script executed:

# Find test specs with broader search
find crates/biome_html_analyze/tests -type d | grep -i "no_aria\|focusable\|a11y"

Repository: biomejs/biome

Length of output: 1914


🏁 Script executed:

# Read the rest of HTML rule to see the is_focusable_element function and lines 165-173
cat -n crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs | tail -n +100

Repository: biomejs/biome

Length of output: 5374


🏁 Script executed:

# Check HTML test specs
ls -la crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/

Repository: biomejs/biome

Length of output: 289


🏁 Script executed:

# Read HTML test spec files
cat crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/invalid.html

Repository: biomejs/biome

Length of output: 799


🏁 Script executed:

cat crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html

Repository: biomejs/biome

Length of output: 738


🏁 Script executed:

# Check if there's a JS test spec for comparison
ls -la crates/biome_js_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/ 2>/dev/null || echo "No JS test spec found"

Repository: biomejs/biome

Length of output: 424


🏁 Script executed:

# Read JS test specs
cat crates/biome_js_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.jsx

Repository: biomejs/biome

Length of output: 367


🏁 Script executed:

cat crates/biome_js_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/invalid.jsx

Repository: biomejs/biome

Length of output: 284


🏁 Script executed:

# Search for "disabled" in both test directories
rg -n "disabled" crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/
rg -n "disabled" crates/biome_js_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/

Repository: biomejs/biome

Length of output: 39


Both rules should check the disabled attribute for natively focusable elements.

Per the HTML spec, disabled form controls (<button disabled>, <input disabled>, etc.) are not focusable and therefore shouldn't trigger this lint when paired with aria-hidden="true". Currently, neither the HTML nor the JS rule checks the disabled attribute—the HTML rule at lines 165–173 returns Some(true) for buttons/inputs without verifying they're enabled. Test coverage for disabled elements is also missing in both implementations.

Recommend adding a check: element.find_attribute_by_name("disabled").is_none() before marking these elements as focusable, and adding test cases like <button disabled aria-hidden="true"> to the valid fixtures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs`
around lines 165 - 173, The natively-focusable check (the block using
name_matches for "button", "select", "textarea", "details", "summary") should
also verify the element is not disabled before returning Some(true); update that
logic (e.g., call element.find_attribute_by_name("disabled").is_none()) so
disabled form controls are treated as unfocusable, and add corresponding tests
(e.g., fixtures with <button disabled aria-hidden="true">) to the valid cases to
cover this behavior for both the Rust rule and the JS equivalent.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@coderabbitai Create an issue (task issue type) as reminder to add this check to both rules

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh issue create \
  --repo biomejs/biome \
  --title "feat(a11y): add disabled attribute check to noAriaHiddenOnFocusable rule (HTML and JS)" \
  --body "## Task

Add a check for the \`disabled\` attribute in the \`noAriaHiddenOnFocusable\` lint rule for both the HTML and JS (JSX) implementations.

## Background

Per the HTML spec, disabled form controls (\`<button disabled>\`, \`<input disabled>\`, \`<select disabled>\`, \`<textarea disabled>\`) are not focusable. Currently, both rules do not account for this and will incorrectly flag \`aria-hidden=\"true\"\` on disabled elements as a violation.

## Changes needed

### HTML rule
File: \`crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs\`

In the \`is_focusable_element\` function, update the natively-focusable check to also verify the element is not disabled before returning \`Some(true)\`. For example:
- Add \`element.find_attribute_by_name(\"disabled\").is_none()\` before marking form controls (button, input, select, textarea) as focusable.

### JS rule
File: \`crates/biome_js_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs\`

Apply the equivalent fix for JSX elements.

### Test fixtures
Add valid test cases for both HTML and JSX, such as:
- \`<button disabled aria-hidden=\"true\"></button>\`
- \`<input disabled aria-hidden=\"true\" />\`
- \`<select disabled aria-hidden=\"true\"></select>\`
- \`<textarea disabled aria-hidden=\"true\"></textarea>\`

## References

- PR: https://github.com/biomejs/biome/pull/9496
- Review comment: https://github.com/biomejs/biome/pull/9496#discussion_r2985561761
- Requested by: `@ematipico`" \
  --assignee ematipico

Length of output: 44


@ematipico, done! I've created the tracking issue here: #9712 — assigned to you, with full context linking back to this PR and review comment.


🧠 Learnings used
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:33.839Z
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.

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:26:56.943Z
Learning: When defining lint rules (declare_lint_rule!), only specify fix_kind if the rule implements an action(...) function. Rules that only emit diagnostics without a code fix should omit fix_kind. This applies to all Rust lint rule definitions under crates/.../src/lint (e.g., crates/biome_js_analyze/src/lint/...).

Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Use the 'issue_number' field in the 'declare_lint_rule!' macro to mark incomplete rules as work in progress, which adds a footnote link to the GitHub issue

Learnt from: dyc3
Repo: biomejs/biome PR: 8639
File: crates/biome_js_analyze/src/lint/nursery/no_excessive_lines_per_file.rs:101-108
Timestamp: 2025-12-31T15:35:41.261Z
Learning: In crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs, the `issue_number` field in `declare_lint_rule!` macro is optional and the vast majority of nursery rules do not need it. Do not recommend adding `issue_number` unless there's a specific reason.

Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Rules with 'recommended: true' and specific domains are only enabled when users enable those domains, while recommended rules without domains are always enabled

Learnt from: dyc3
Repo: biomejs/biome PR: 9617
File: crates/biome_html_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rs:58-65
Timestamp: 2026-03-25T13:49:08.724Z
Learning: In Biome’s `biome_html_analyze` HTML element-name checks inside lint rules, the casing of matches must depend on the input file type:
- For `.html` files, compare element names case-insensitively.
- For `.astro`, `.vue`, and `.svelte` files, compare case-sensitively.

Reason: in template languages (Astro/Vue/Svelte), PascalCase typically denotes custom components, while lowercase names denote native HTML elements. Case-sensitive matching is required to avoid misclassifying components as native elements (and vice versa).

Learnt from: ematipico
Repo: biomejs/biome PR: 9416
File: crates/biome_service/src/file_handlers/html.rs:850-868
Timestamp: 2026-03-09T15:54:24.948Z
Learning: In `crates/biome_service/src/file_handlers/html.rs` (biomejs/biome), Svelte `on:` event directives (e.g., `on:click={...}`) are old Svelte 3/4 syntax and are intentionally not supported by Biome. Svelte 5 runes mode uses regular attributes for event handlers instead. Do not flag missing handling of Svelte `on:` directives as an issue.

Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Add a 'deprecated' field to 'declare_lint_rule!' macro when deprecating a rule to communicate the reason for deprecation (e.g., suggesting an alternative rule)

Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Set rule severity to 'error' for rules in 'correctness', 'security', and 'a11y' groups; use 'warn' or 'info' for other groups based on rule type

Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Add 'fix_kind: FixKind::Safe' or 'fix_kind: FixKind::Unsafe' to the 'declare_lint_rule!' macro when implementing the 'action' function

Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2026-03-21T17:22:03.949Z
Learning: Applies to crates/biome_analyze/**/lint/**/*.rs : Document rule options in a '## Options' section with h3 headers for each option, describing what it does, its default value, and providing code examples

Learnt from: dyc3
Repo: biomejs/biome PR: 8901
File: crates/biome_analyze_macros/src/group_macro.rs:9-9
Timestamp: 2026-01-29T22:10:30.888Z
Learning: Do not import xtask inside biome crates. In the biomeps repository, ensure that biome crates (e.g., crates/biome_*/...) do not have imports like use xtask_glue::... or other xtask-related crates. The correct dependency direction is for xtask to depend on and import biome crates, not the reverse, since xtask contains heavier utilities (e.g., git2) that can inflate build times when included in biome crates. When reviewing, verify dependency declarations (Cargo.toml) and any use statements to confirm xtask crates are not pulled into biome crates; prefer having xtask depend on biome crates and export needed functionality through stable interfaces.

Copy link
Copy Markdown
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

Still comments not addressed. Tests with Vue/astro/svelte with components too missing

Comment thread .changeset/port-no-aria-hidden-on-focusable-html.md Outdated
@aviraldua93 aviraldua93 force-pushed the feat/html-no-aria-hidden-on-focusable branch from 1ffe6b4 to 405e77c Compare March 29, 2026 22:05
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html (1)

1-17: Good test coverage for valid (non-diagnostic) cases.

The fixture covers key scenarios: aria-hidden on generic elements, negative tabindex, type="hidden" inputs, contenteditable="false", anchors without href, uppercase tags, and custom components.

Consider adding a test for disabled form controls once that check is implemented:

<button disabled aria-hidden="true">disabled button</button>
<input disabled aria-hidden="true" />
🤖 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/noAriaHiddenOnFocusable/valid.html`
around lines 1 - 17, Add coverage for disabled form controls by extending the
valid.html fixture to include a disabled button and a disabled input that both
have aria-hidden="true" so the test asserts no diagnostics for disabled
controls; locate the existing examples with BUTTON/BUTTON, <input> and <button>
entries (e.g., the BUTTON aria-hidden cases and input type="hidden" case) and
add corresponding disabled cases (a disabled button and a disabled input)
adjacent to them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html`:
- Around line 1-17: Add coverage for disabled form controls by extending the
valid.html fixture to include a disabled button and a disabled input that both
have aria-hidden="true" so the test asserts no diagnostics for disabled
controls; locate the existing examples with BUTTON/BUTTON, <input> and <button>
entries (e.g., the BUTTON aria-hidden cases and input type="hidden" case) and
add corresponding disabled cases (a disabled button and a disabled input)
adjacent to them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cf809a62-d336-446d-909d-8947fcdd569e

📥 Commits

Reviewing files that changed from the base of the PR and between 1ffe6b4 and 405e77c.

📒 Files selected for processing (10)
  • .changeset/port-no-aria-hidden-on-focusable-html.md
  • crates/biome_html_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/astro/valid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/svelte/invalid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/svelte/valid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/valid.html
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/vue/valid.vue
✅ Files skipped from review due to trivial changes (7)
  • .changeset/port-no-aria-hidden-on-focusable-html.md
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/vue/valid.vue
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/astro/valid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/svelte/valid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/svelte/invalid.svelte

@aviraldua93 aviraldua93 force-pushed the feat/html-no-aria-hidden-on-focusable branch from 405e77c to 49252ba Compare March 29, 2026 22:20
@aviraldua93
Copy link
Copy Markdown
Author

Thanks for the patience @ematipico — all feedback addressed in the latest push:

  • Changeset: bumped to minor, added example snippet
  • Let-chain: tabindex check uses let-chain (lines 77-79)
  • i32: confirmed JSX rule uses same type
  • Docstrings: business logic explained on all helpers
  • Option<bool>: is_focusable_element returns Option<bool> with ? operator
  • text_trimmed: investigated — element.name() returns TokenText built from token_text_trimmed() internally, so .text() is trivia-free (same pattern as use_button_type.rs, use_alt_text.rs, etc.). Happy to change if you prefer a different approach.
  • Case sensitivity + components: covered in HTML tests
  • Vue/Svelte/Astro: added 6 framework test files with subdirectory layout (matching useAnchorContent, useMediaCaption)
  • Snapshots: generated locally via cargo insta, all 8 tests pass
  • Clippy: clean, zero warnings

Replied to each inline comment individually as well.

@siketyan
Copy link
Copy Markdown
Member

siketyan commented Mar 30, 2026

I think you are using some kind of LLMs to generate code and/or making replies. We require disclosure of any AI/LLMs assistance in the PR description

Comment thread crates/biome_html_analyze/tests/specs/a11y/noAriaHiddenOnFocusable/vue/valid.vue Outdated
Port the noAriaHiddenOnFocusable lint rule from JSX to HTML. The rule
enforces that aria-hidden=true is not set on focusable elements.

Addresses all review feedback:
- Use let-chains for tabindex checking
- Return Option<bool> from is_focusable_element with ? operator
- Business-logic doc comments on all helper functions
- Exclude input type=hidden from focusability (HTML spec)
- Tighten contenteditable to HTML spec enumerated states:
  only true/empty/plaintext-only are editing hosts
- Case sensitivity tests (BUTTON, INPUT uppercase)
- Component tests (MyButton - not flagged)
- Framework test coverage: Vue, Svelte, Astro (valid + invalid)
- Changeset bumped to minor with example snippet
- All snapshots generated via cargo insta
- Clippy clean, all 8 tests pass

Part of biomejs#8155.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@aviraldua93 aviraldua93 force-pushed the feat/html-no-aria-hidden-on-focusable branch from 31d4e19 to c183dca Compare March 30, 2026 15:36
@github-actions github-actions bot added the L-JavaScript Language: JavaScript and super languages label Apr 6, 2026
@ematipico ematipico merged commit 1dfb829 into biomejs:next Apr 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter L-HTML Language: HTML and super languages L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants