Skip to content
This repository was archived by the owner on Apr 16, 2026. It is now read-only.

Commit d588394

Browse files
committed
Fix image-alt-words to only flag prefix patterns
Changed from word-anywhere matching to prefix-only regex. Now only flags when words like "image", "photo", "icon" appear at the start of alt text followed by "of", a separator, or as the entire text. No longer flags natural usage like "Close icon for closing window" or "Walmart Photo logo". Bump version to 0.8.5.
1 parent 134550d commit d588394

3 files changed

Lines changed: 90 additions & 17 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@accesslint/core",
3-
"version": "0.8.4",
3+
"version": "0.8.5",
44
"description": "Pure accessibility rule engine — WCAG audit with zero browser dependencies",
55
"type": "module",
66
"main": "./dist/index.cjs",

src/rules/text-alternatives/image-alt-words.test.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,55 @@ import { imageAltWords } from "./image-alt-words";
33
import { makeDoc } from "../../test-helpers";
44

55
describe("text-alternatives/image-alt-words", () => {
6-
it("reports alt text containing 'image'", () => {
6+
// --- Should flag (prefix patterns) ---
7+
it("reports alt starting with 'image of'", () => {
78
const doc = makeDoc('<img src="dog.jpg" alt="image of a dog">');
89
const violations = imageAltWords.run(doc);
910
expect(violations).toHaveLength(1);
1011
expect(violations[0].ruleId).toBe("text-alternatives/image-alt-words");
1112
});
1213

13-
it("reports alt text containing 'photo'", () => {
14+
it("reports alt starting with 'photo of'", () => {
1415
const doc = makeDoc('<img src="team.jpg" alt="photo of team">');
1516
expect(imageAltWords.run(doc)).toHaveLength(1);
1617
});
1718

18-
it("reports alt text containing 'picture'", () => {
19+
it("reports alt starting with 'picture of'", () => {
1920
const doc = makeDoc('<img src="sunset.jpg" alt="picture of sunset">');
2021
expect(imageAltWords.run(doc)).toHaveLength(1);
2122
});
2223

24+
it("reports alt starting with 'graphic of'", () => {
25+
const doc = makeDoc('<img src="chart.png" alt="graphic of sales data">');
26+
expect(imageAltWords.run(doc)).toHaveLength(1);
27+
});
28+
29+
it("reports alt that is just 'icon'", () => {
30+
const doc = makeDoc('<img src="x.png" alt="icon">');
31+
expect(imageAltWords.run(doc)).toHaveLength(1);
32+
});
33+
34+
it("reports alt that is just 'image'", () => {
35+
const doc = makeDoc('<img src="x.png" alt="image">');
36+
expect(imageAltWords.run(doc)).toHaveLength(1);
37+
});
38+
39+
it("reports alt with separator 'photo: sunset'", () => {
40+
const doc = makeDoc('<img src="x.jpg" alt="photo: sunset">');
41+
expect(imageAltWords.run(doc)).toHaveLength(1);
42+
});
43+
44+
it("reports alt with dash 'icon - search'", () => {
45+
const doc = makeDoc('<img src="x.png" alt="icon - search">');
46+
expect(imageAltWords.run(doc)).toHaveLength(1);
47+
});
48+
49+
it("reports case-insensitive 'Image Of a dog'", () => {
50+
const doc = makeDoc('<img src="x.jpg" alt="Image Of a dog">');
51+
expect(imageAltWords.run(doc)).toHaveLength(1);
52+
});
53+
54+
// --- Should NOT flag ---
2355
it("passes alt text without redundant words", () => {
2456
const doc = makeDoc('<img src="dog.jpg" alt="Golden retriever playing fetch">');
2557
expect(imageAltWords.run(doc)).toHaveLength(0);
@@ -30,8 +62,38 @@ describe("text-alternatives/image-alt-words", () => {
3062
expect(imageAltWords.run(doc)).toHaveLength(0);
3163
});
3264

33-
it("does not flag partial word matches", () => {
65+
it("does not flag partial word matches like 'Imagination'", () => {
3466
const doc = makeDoc('<img src="x.jpg" alt="Imagination is key">');
3567
expect(imageAltWords.run(doc)).toHaveLength(0);
3668
});
69+
70+
it("skips 'icon' in middle: 'Close icon for closing window'", () => {
71+
const doc = makeDoc('<img src="x.png" alt="Close icon for closing window">');
72+
expect(imageAltWords.run(doc)).toHaveLength(0);
73+
});
74+
75+
it("skips 'photo' as brand name: 'Walmart Photo logo'", () => {
76+
const doc = makeDoc('<img src="x.png" alt="Walmart Photo logo">');
77+
expect(imageAltWords.run(doc)).toHaveLength(0);
78+
});
79+
80+
it("skips 'icon' at end: 'magnifying glass icon'", () => {
81+
const doc = makeDoc('<img src="x.png" alt="magnifying glass icon">');
82+
expect(imageAltWords.run(doc)).toHaveLength(0);
83+
});
84+
85+
it("skips 'photo' in natural sentence: 'A family taking a photo'", () => {
86+
const doc = makeDoc('<img src="x.jpg" alt="A family taking a photo">');
87+
expect(imageAltWords.run(doc)).toHaveLength(0);
88+
});
89+
90+
it("skips 'image' not at start: 'Brand image gallery'", () => {
91+
const doc = makeDoc('<img src="x.jpg" alt="Brand image gallery">');
92+
expect(imageAltWords.run(doc)).toHaveLength(0);
93+
});
94+
95+
it("skips 'icon' followed by a regular word: 'Icon set for navigation'", () => {
96+
const doc = makeDoc('<img src="x.png" alt="Icon set for navigation">');
97+
expect(imageAltWords.run(doc)).toHaveLength(0);
98+
});
3799
});

src/rules/text-alternatives/image-alt-words.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import type { Rule } from "../types";
22
import { getSelector, getHtmlSnippet } from "../utils/selector";
33

4-
const REDUNDANT_WORDS = ["image", "picture", "photo", "graphic", "icon", "img"];
4+
/**
5+
* Matches alt text that starts with a self-referential word used as a prefix.
6+
* Only flags when the word appears at the start and is followed by a
7+
* preposition ("of"), a separator (: - — –), or is the entire alt text.
8+
*
9+
* Flags: "image of a dog", "photo: sunset", "icon", "graphic"
10+
* Skips: "Close icon for closing window", "Walmart Photo logo",
11+
* "magnifying glass icon", "A family taking a photo"
12+
*/
13+
const REDUNDANT_PREFIX_RE =
14+
/^(image|picture|photo|graphic|icon|img)(\s+of\b|\s*[:\u2013\u2014-]|\s*$)/i;
515

616
/**
7-
* Checks if alt text contains self-referential words like "image" or "photo".
8-
* Screen readers already announce "image"/"graphic" before alt text, so these
9-
* words are redundant. Separate from image-redundant-alt because axe-core
10-
* does not have an equivalent check.
17+
* Checks if alt text starts with a self-referential prefix like "image of"
18+
* or "photo:". Screen readers already announce "image"/"graphic" before alt
19+
* text, so these prefixes are redundant. Separate from image-redundant-alt
20+
* because axe-core does not have an equivalent check.
1121
*/
1222
export const imageAltWords: Rule = {
1323
id: "text-alternatives/image-alt-words",
@@ -18,25 +28,26 @@ export const imageAltWords: Rule = {
1828
fixability: "contextual",
1929
browserHint: "Screenshot the image to verify the alt text accurately describes it without filler words like 'image of'.",
2030
description:
21-
"Image alt text should not contain words like 'image', 'photo', or 'picture' — screen readers already announce the element type.",
31+
"Image alt text should not start with words like 'image of', 'photo of', or 'picture of' — screen readers already announce the element type.",
2232
guidance:
2333
"Screen readers already announce 'image' or 'graphic' before reading alt text, so phrases like 'image of', 'photo of', or 'picture of' are redundant. Remove these words and describe what the image shows. For example, change 'image of a dog' to 'golden retriever playing fetch'.",
2434
run(doc) {
2535
const violations = [];
2636
for (const img of doc.querySelectorAll("img[alt]")) {
27-
const alt = img.getAttribute("alt")!.toLowerCase();
37+
const alt = img.getAttribute("alt")!.trim();
2838
if (!alt) continue;
2939

30-
const found = REDUNDANT_WORDS.filter((w) => alt.split(/\s+/).includes(w));
31-
if (found.length > 0) {
40+
const match = alt.match(REDUNDANT_PREFIX_RE);
41+
if (match) {
42+
const word = match[1].toLowerCase();
3243
violations.push({
3344
ruleId: "text-alternatives/image-alt-words",
3445
selector: getSelector(img),
3546
html: getHtmlSnippet(img),
3647
impact: "minor" as const,
37-
message: `Alt text "${img.getAttribute("alt")}" contains redundant word(s): ${found.join(", ")}.`,
38-
context: `Current alt: "${img.getAttribute("alt")}", redundant word(s): ${found.join(", ")}`,
39-
fix: { type: "suggest", suggestion: "Remove the redundant word(s) from the alt text; screen readers already announce the element as an image" } as const,
48+
message: `Alt text "${alt}" starts with redundant prefix "${word}".`,
49+
context: `Current alt: "${alt}", redundant prefix: "${word}"`,
50+
fix: { type: "suggest", suggestion: "Remove the redundant prefix from the alt text; screen readers already announce the element as an image" } as const,
4051
});
4152
}
4253
}

0 commit comments

Comments
 (0)