JS-1465 Fix S6819 false positive: SVG role="img" with accessible name#6856
Conversation
Tests cover the scenario where inline SVG elements with role="img" and a proper accessible name (via <title> child, aria-label, or aria-labelledby) are incorrectly flagged as violations suggesting <img> replacement. The tests verify that the decorator suppresses reports for these WCAG-compliant patterns while still flagging SVG elements with role="img" but no accessible name. Relates to JS-1465
The rule was incorrectly flagging inline SVG elements with role="img" that have proper accessible names via <title> child elements, aria-label, or aria-labelledby attributes. This is a WCAG-compliant pattern for icon components that require CSS class control, animation, or programmatic styling — capabilities that native <img> tags cannot provide. Extends decorator.ts with a new isSemanticSvgImg() helper that suppresses reports for SVG elements with role="img" when they have an accessible name. SVG elements with role="img" but no accessible name continue to be flagged as true positives. Also adds a compliant code example to the rule spec. Implementation follows the approved proposal guidelines from JS-1465. Relates to JS-1465
Rule Profile
A genuine violation — an SVG with <svg role="img" viewBox="0 0 24 24">
<path d="M5 12h14"/>
</svg>
// ^ S6819: Use <img alt=...> instead of the "img" roleFalse Positive Pattern
// FP 1: accessible name via <title> child
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Nx</title>
<path d="M11.987 14.138l-3.132 4.923..." />
</svg>
// FP 2: accessible name via aria-label
<svg role="img" aria-label="GitHub" viewBox="0 0 24 24">
<path d="..." />
</svg>
// FP 3: accessible name via aria-labelledby
<svg role="img" aria-labelledby="icon-title" viewBox="0 0 24 24">
<title id="icon-title">Download</title>
<path d="..." />
</svg>False Negative RiskThe guard is conservative: suppression only fires when
The one truly conservative choice is dynamic expressions — Fix SummaryThe implementation adds two pure functions to
|
Ruling Report✅ No changes to ruling expected issues in this PR |
SummaryThis PR fixes a false positive in S6819 (prefer-tag-over-role) by recognizing inline SVG with What reviewers should knowWhere to start: Look at Key implementation details:
Test coverage: The unit test comprehensively covers both the decorator's suppression (valid SVGs that should not be flagged) and the upstream rule's behavior (which still raises errors on these cases, proving the decorator suppression is working). Test cases include edge cases like empty
|
…rc/jsts/rules/S6819/decorator.ts Comment: `getProp` returns the prop AST node whenever the attribute is present, regardless of its value. This means `<svg role="img" aria-label="">` (explicit empty string — no accessible name) would still satisfy the guard and suppress the report, producing a false negative. Contrast with `isDecorativeSvg` just above, which correctly uses `getLiteralPropValue` to inspect the actual value before accepting the prop. For dynamic expressions (`aria-label={someVar}`), `getLiteralPropValue` returns `null`, which is fine — we can't statically determine the value so suppressing is the safe choice. Only the literal empty-string case is broken. ```suggestion const ariaLabelProp = getProp(attributes, 'aria-label'); if (ariaLabelProp && getLiteralPropValue(ariaLabelProp) !== '') { return true; } const ariaLabelledbyProp = getProp(attributes, 'aria-labelledby'); if (ariaLabelledbyProp && getLiteralPropValue(ariaLabelledbyProp) !== '') { return true; } ``` - [ ] Mark as noise
…rc/jsts/rules/S6819/unit.test.ts Comment: The `invalid` block only tests SVG with no accessible-name attributes at all. There is no test for `<svg role="img" aria-label="">` (explicit empty string), which is the edge case the guard currently mishandles. Adding it here would both document the expected behaviour and catch a regression if the guard is ever simplified back to a bare `getProp` check. ```suggestion invalid: [ // svg role="img" without an accessible name is still a true positive { code: `<svg role="img" viewBox="0 0 24 24"><path d="M5 12h14"/></svg>`, errors: 1, }, // svg role="img" with an explicit empty aria-label provides no accessible name { code: `<svg role="img" aria-label=""><path d="M5 12h14"/></svg>`, errors: 1, }, ], ``` - [ ] Mark as noise
|
…rc/jsts/rules/S6819/decorator.ts Comment: `hasTitleChild` checks for the *presence* of a `<title>` child but not whether it has any content. `<svg role="img"><title></title></svg>` passes the guard and suppresses the report even though an empty `<title>` provides no accessible name — the exact same false-negative class as the `aria-label=""` issue fixed in this commit. The fix should match the treatment of `aria-label`: accept a `<title>` only when it has at least one non-whitespace text child or a dynamic expression (which we can't statically evaluate, so we conservatively suppress). ```suggestion return parent.children.some( child => child.type === 'JSXElement' && child.openingElement.name.type === 'JSXIdentifier' && child.openingElement.name.name === 'title' && child.children.some( c => (c.type === 'JSXText' && c.value.trim() !== '') || c.type === 'JSXExpressionContainer', ), ); ``` - [ ] Mark as noise
s6819-inline-svg-with-roleimg-is-semantically-correct-sonnet
| const ariaLabelProp = getProp(attributes, 'aria-label'); | ||
| if (ariaLabelProp && getLiteralPropValue(ariaLabelProp) !== '') { | ||
| return true; | ||
| } | ||
| const ariaLabelledbyProp = getProp(attributes, 'aria-labelledby'); | ||
| if (ariaLabelledbyProp && getLiteralPropValue(ariaLabelledbyProp) !== '') { |
There was a problem hiding this comment.
getLiteralPropValue returns null for any non-literal prop value — dynamic expressions like aria-label={someVar}, aria-label={null}, or aria-label={undefined}. Because null !== '' is true, those cases all fall through and suppress the report.
This is intentionally more permissive than isDecorativeSvg, which uses === true || === 'true' (so a dynamic aria-hidden does not suppress). For aria-label, accepting unknowable dynamic values is a reasonable static-analysis trade-off — but aria-label={null} is a real pattern in conditional rendering (e.g. aria-label={active ? 'Close' : null}) and React silently drops the attribute at runtime, leaving the SVG with no accessible name.
Two things worth confirming:
- Is suppressing on any dynamic
aria-label/aria-labelledbyintentional, or should it require a string literal (mirroring howisDecorativeSvghandlesaria-hidden)? - If dynamic suppression is intentional, a test case documenting it (
aria-label={someVar}in thevalidblock) would pin the behaviour and serve as an explicit notice that it differs from the decorative-SVG guard.
| const ariaLabelProp = getProp(attributes, 'aria-label'); | |
| if (ariaLabelProp && getLiteralPropValue(ariaLabelProp) !== '') { | |
| return true; | |
| } | |
| const ariaLabelledbyProp = getProp(attributes, 'aria-labelledby'); | |
| if (ariaLabelledbyProp && getLiteralPropValue(ariaLabelledbyProp) !== '') { | |
| const ariaLabelProp = getProp(attributes, 'aria-label'); | |
| const ariaLabelValue = ariaLabelProp ? getLiteralPropValue(ariaLabelProp) : null; | |
| if (typeof ariaLabelValue === 'string' && ariaLabelValue !== '') { | |
| return true; | |
| } | |
| const ariaLabelledbyProp = getProp(attributes, 'aria-labelledby'); | |
| const ariaLabelledbyValue = ariaLabelledbyProp ? getLiteralPropValue(ariaLabelledbyProp) : null; | |
| if (typeof ariaLabelledbyValue === 'string' && ariaLabelledbyValue !== '') { | |
| return true; | |
| } |
- Mark as noise




Relates to JS-1465
S6819 was incorrectly flagging
<svg role="img">elements that carry a proper accessible name — a<title>child,aria-label, oraria-labelledbyattribute. These are valid WCAG-compliant patterns for inline SVG icons that need CSS control or animation that a native<img>cannot provide. The fix adds a tightly scopedisSemanticSvgImg()guard in the decorator that suppresses the report only when bothrole="img"and a non-empty accessible name are present; SVGs withrole="img"but no accessible name remain true positives.aria-label/aria-labelledbyare correctly treated as missing (no suppression), matching howisDecorativeSvgalready handles attribute values.<title>suppression requires at least one non-whitespace text child or a dynamic expression child — an empty<title>does not suppress.