Environment information
Details
CLI:
Version: 2.5.14
Color support: false
Platform:
CPU Architecture: aarch64
OS: macos
Environment:
BIOME_DISTRIBUTION: npm
BIOME_LOG_FILE: unset
BIOME_LOG_PATH: unset
BIOME_LOG_PREFIX_NAME: unset
BIOME_LOG_LEVEL: unset
BIOME_LOG_KIND: unset
BIOME_CONFIG_PATH: unset
BIOME_THREADS: unset
BIOME_WATCHER_KIND: unset
BIOME_WATCHER_POLLING_INTERVAL: unset
BIOME_BINARY: unset
RUST_BACKTRACE: unset
NO_COLOR: unset
TERM: unset
JS_RUNTIME_VERSION: v24.21.0
JS_RUNTIME_NAME: node
NODE_PACKAGE_MANAGER: pnpm/10.30.1
Biome Configuration:
Status: Loaded successfully.
Path: biome.json
Formatter enabled: true
Linter enabled: true
Assist enabled: true
VCS enabled: false
HTML full support enabled: true
Linter:
JavaScript enabled: unset
JSON enabled: unset
CSS enabled: unset
GraphQL enabled: unset
Recommended: unset
Enabled rules:
a11y/noAccessKey
a11y/noAmbiguousAnchorText
a11y/noAriaHiddenOnFocusable
a11y/noAriaUnsupportedElements
a11y/noAutofocus
a11y/noDistractingElements
a11y/noHeaderScope
a11y/noInteractiveElementToNoninteractiveRole
a11y/noLabelWithoutControl
a11y/noNoninteractiveElementToInteractiveRole
a11y/noNoninteractiveTabindex
a11y/noPositiveTabindex
a11y/noRedundantAlt
a11y/noRedundantRoles
a11y/noStaticElementInteractions
a11y/noSvgWithoutTitle
a11y/useAltText
a11y/useAnchorContent
a11y/useAriaActivedescendantWithTabindex
a11y/useAriaPropsForRole
a11y/useAriaPropsSupportedByRole
a11y/useButtonType
a11y/useFocusableInteractive
a11y/useGenericFontNames
a11y/useHeadingContent
a11y/useHtmlLang
a11y/useIframeTitle
a11y/useKeyWithClickEvents
a11y/useKeyWithMouseEvents
a11y/useMediaCaption
a11y/useSemanticElements
a11y/useValidAnchor
a11y/useValidAriaProps
a11y/useValidAriaRole
a11y/useValidAriaValues
a11y/useValidAutocomplete
a11y/useValidLang
complexity/noAdjacentSpacesInRegex
complexity/noArguments
complexity/noBannedTypes
complexity/noCommaOperator
complexity/noEmptyTypeParameters
complexity/noExtraBooleanCast
complexity/noFlatMapIdentity
complexity/noImportantStyles
complexity/noStaticOnlyClass
complexity/noThisInStatic
complexity/noUselessCatch
complexity/noUselessConstructor
complexity/noUselessContinue
complexity/noUselessEmptyExport
complexity/noUselessEscapeInRegex
complexity/noUselessFragments
complexity/noUselessLabel
complexity/noUselessLoneBlockStatements
complexity/noUselessRename
complexity/noUselessStringRaw
complexity/noUselessSwitchCase
complexity/noUselessTernary
complexity/noUselessThisAlias
complexity/noUselessTypeConstraint
complexity/noUselessUndefinedInitialization
complexity/useArrowFunction
complexity/useDateNow
complexity/useFlatMap
complexity/useIndexOf
complexity/useLiteralKeys
complexity/useNumericLiterals
complexity/useOptionalChain
complexity/useRegexLiterals
complexity/useSimpleNumberKeys
correctness/noConstAssign
correctness/noConstantCondition
correctness/noConstantMathMinMaxClamp
correctness/noConstructorReturn
correctness/noDuplicateArgumentNames
correctness/noDuplicateAttributes
correctness/noDuplicateEnumValueNames
correctness/noDuplicateInputFieldNames
correctness/noDuplicateVariableNames
correctness/noEmptyCharacterClassInRegex
correctness/noEmptyPattern
correctness/noGlobalObjectCalls
correctness/noInnerDeclarations
correctness/noInvalidBuiltinInstantiation
correctness/noInvalidConstructorSuper
correctness/noInvalidDirectionInLinearGradient
correctness/noInvalidGridAreas
correctness/noInvalidPositionAtImportRule
correctness/noInvalidUseBeforeDeclaration
correctness/noMissingVarFunction
correctness/noNonoctalDecimalEscape
correctness/noPrecisionLoss
correctness/noSelfAssign
correctness/noSetterReturn
correctness/noStringCaseMismatch
correctness/noSwitchDeclarations
correctness/noUnknownFunction
correctness/noUnknownMediaFeatureName
correctness/noUnknownProperty
correctness/noUnknownPseudoClass
correctness/noUnknownPseudoElement
correctness/noUnknownTypeSelector
correctness/noUnknownUnit
correctness/noUnmatchableAnbSelector
correctness/noUnreachable
correctness/noUnreachableSuper
correctness/noUnsafeFinally
correctness/noUnsafeOptionalChaining
correctness/noUnusedFunctionParameters
correctness/noUnusedImports
correctness/noUnusedLabels
correctness/noUnusedPrivateClassMembers
correctness/noUnusedVariables
correctness/noVoidElementsWithChildren
correctness/noVoidTypeReturn
correctness/useGraphqlNamedOperations
correctness/useIsNan
correctness/useLoneAnonymousOperation
correctness/useParseIntRadix
correctness/useValidForDirection
correctness/useValidTypeof
correctness/useYield
performance/noAccumulatingSpread
performance/noDynamicNamespaceImportAccess
security/noBlankTarget
security/noGlobalEval
security/noScriptUrl
style/noDescendingSpecificity
style/noNonNullAssertion
style/useArrayLiterals
style/useConst
style/useDeprecatedReason
style/useExponentiationOperator
style/useExportType
style/useImportType
style/useLiteralEnumMembers
style/useNodejsImportProtocol
style/useShorthandFunctionType
style/useTemplate
suspicious/noApproximativeNumericConstant
suspicious/noAssignInExpressions
suspicious/noAsyncPromiseExecutor
suspicious/noBiomeFirstException
suspicious/noCatchAssign
suspicious/noClassAssign
suspicious/noCommentText
suspicious/noCompareNegZero
suspicious/noConfusingLabels
suspicious/noConfusingVoidType
suspicious/noConstEnum
suspicious/noControlCharactersInRegex
suspicious/noDebugger
suspicious/noDeprecatedMediaType
suspicious/noDocumentCookie
suspicious/noDoubleEquals
suspicious/noDuplicateAtImportRules
suspicious/noDuplicateCase
suspicious/noDuplicateClassMembers
suspicious/noDuplicateCustomProperties
suspicious/noDuplicateElseIf
suspicious/noDuplicateEnumValues
suspicious/noDuplicateFields
suspicious/noDuplicateFontNames
suspicious/noDuplicateJsxProps
suspicious/noDuplicateObjectKeys
suspicious/noDuplicateParameters
suspicious/noDuplicateProperties
suspicious/noDuplicateSelectorsKeyframeBlock
suspicious/noEmptyBlock
suspicious/noEmptyInterface
suspicious/noExplicitAny
suspicious/noExtraNonNullAssertion
suspicious/noFallthroughSwitchClause
suspicious/noFunctionAssign
suspicious/noGlobalAssign
suspicious/noGlobalIsFinite
suspicious/noGlobalIsNan
suspicious/noImplicitAnyLet
suspicious/noImportAssign
suspicious/noImportantInKeyframe
suspicious/noIrregularWhitespace
suspicious/noLabelVar
suspicious/noMisleadingCharacterClass
suspicious/noMisleadingInstantiator
suspicious/noMisrefactoredShorthandAssign
suspicious/noNonNullAssertedOptionalChain
suspicious/noOctalEscape
suspicious/noProto
suspicious/noPrototypeBuiltins
suspicious/noQuickfixBiome
suspicious/noRedeclare
suspicious/noRedundantUseStrict
suspicious/noSelfCompare
suspicious/noShadowRestrictedNames
suspicious/noShorthandPropertyOverrides
suspicious/noSparseArray
suspicious/noSuspiciousSemicolonInJsx
suspicious/noTemplateCurlyInString
suspicious/noThenProperty
suspicious/noTsIgnore
suspicious/noUnknownAtRules
suspicious/noUnsafeDeclarationMerging
suspicious/noUnsafeNegation
suspicious/noUselessEscapeInString
suspicious/noUselessRegexBackrefs
suspicious/noWith
suspicious/useAdjacentOverloadSignatures
suspicious/useBiomeIgnoreFolder
suspicious/useDefaultSwitchClauseLast
suspicious/useGetterReturn
suspicious/useGoogleFontDisplay
suspicious/useIsArray
suspicious/useIterableCallbackReturn
suspicious/useNamespaceKeyword
Workspace:
Open Documents: 0
Rule name
lint/a11y/useAnchorContent
Playground link
https://biomejs.dev/playground/?pane=Diagnostics&language=astro#code=PABhACAAaAByAGUAZgA9ACIALwB4ACIAPgA8AEkAYwBvAG4AIAAvAD4APAAvAGEAPgAKADwAYQAgAGgAcgBlAGYAPQAiAC8AeQAiAD4APABJAGMAbwBuAD4APAAvAEkAYwBvAG4APgA8AC8AYQA%2BAAoA
Expected result
useAnchorContent already treats a PascalCase component as possible accessible content, but only when the component is written as a self-closing element. Written with an explicit closing tag — the same thing in Astro, Vue and Svelte — the anchor is reported as empty.
main.astro:
<a href="/x"><Icon /></a>
<a href="/y"><Icon></Icon></a>
$ biome lint main.astro
main.astro:2:1 lint/a11y/useAnchorContent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Provide screen reader accessible content when using a elements.
Found 1 error.
Only line 2 is reported. Biome cannot know what <Icon> renders in either spelling, so I would expect neither line to be reported — and in any case I would expect the two spellings to be treated the same.
The equivalent JSX reports neither spelling:
export const A = () => <a href="/x"><Icon /></a>;
export const B = () => <a href="/y"><Icon></Icon></a>;
$ biome lint main.jsx
Checked 1 file in 1191µs. No fixes applied.
The changelog entry for #11790 gives "matching JSX behavior" as the goal, so the difference looks unintended.
This is not a regression: the asymmetry behaves the same in 2.5.13 and still reproduces on next (build 137d48a). What changed in 2.5.14 is the removal of the early return for anchors that have an accessible name (#11790), which used to mask it:
main.astro above |
2.5.13 |
2.5.14 |
| as written |
1 error |
1 error |
with aria-label on both anchors |
0 errors |
1 error |
Where it comes from
In crates/biome_html_analyze/src/lint/a11y/use_anchor_content.rs, has_accessible_content handles the two element kinds differently. The self-closing branch inspects the tag name:
AnyHtmlElement::HtmlSelfClosingElement(element) => {
// ...
match tag_text.as_ref().map(|t| t.as_ref()) {
// ...
// Custom components (PascalCase) may render accessible content
Some(name) if name.starts_with(|c: char| c.is_uppercase()) => true,
_ => false,
}
}
The paired-tag branch never looks at the name and recurses into the children, so a component with no children evaluates to false:
AnyHtmlElement::HtmlElement(element) => {
if html_element_has_truthy_aria_hidden(element) {
false
} else {
has_accessible_content(&element.children(), is_astro)
}
}
The Astro Image case added in #9270 / #9369 lives in the same self-closing branch, so <a><Image alt="x"></Image></a> is reported while <a><Image alt="x" /></a> is not.
Related
#10663 / #10699 report and fix the same shape of asymmetry for <slot>, adding the check to both branches. #9270 / #9369 added the Astro Image case to the self-closing branch. This issue is about the PascalCase component case, which the paired-tag branch still misses.
Code of Conduct
Environment information
Details
Rule name
lint/a11y/useAnchorContent
Playground link
https://biomejs.dev/playground/?pane=Diagnostics&language=astro#code=PABhACAAaAByAGUAZgA9ACIALwB4ACIAPgA8AEkAYwBvAG4AIAAvAD4APAAvAGEAPgAKADwAYQAgAGgAcgBlAGYAPQAiAC8AeQAiAD4APABJAGMAbwBuAD4APAAvAEkAYwBvAG4APgA8AC8AYQA%2BAAoA
Expected result
useAnchorContentalready treats a PascalCase component as possible accessible content, but only when the component is written as a self-closing element. Written with an explicit closing tag — the same thing in Astro, Vue and Svelte — the anchor is reported as empty.main.astro:Only line 2 is reported. Biome cannot know what
<Icon>renders in either spelling, so I would expect neither line to be reported — and in any case I would expect the two spellings to be treated the same.The equivalent JSX reports neither spelling:
The changelog entry for #11790 gives "matching JSX behavior" as the goal, so the difference looks unintended.
This is not a regression: the asymmetry behaves the same in 2.5.13 and still reproduces on
next(build137d48a). What changed in 2.5.14 is the removal of the early return for anchors that have an accessible name (#11790), which used to mask it:main.astroabovearia-labelon both anchorsWhere it comes from
In
crates/biome_html_analyze/src/lint/a11y/use_anchor_content.rs,has_accessible_contenthandles the two element kinds differently. The self-closing branch inspects the tag name:The paired-tag branch never looks at the name and recurses into the children, so a component with no children evaluates to
false:The Astro
Imagecase added in #9270 / #9369 lives in the same self-closing branch, so<a><Image alt="x"></Image></a>is reported while<a><Image alt="x" /></a>is not.Related
#10663 / #10699 report and fix the same shape of asymmetry for
<slot>, adding the check to both branches. #9270 / #9369 added the AstroImagecase to the self-closing branch. This issue is about the PascalCase component case, which the paired-tag branch still misses.Code of Conduct