网页图片的无障碍实战:alt 到底怎么写,屏幕阅读器里那些图是什么样的

📅 2026/7/14 18:26:40 👁️ 阅读次数 📝 编程学习
网页图片的无障碍实战:alt 到底怎么写,屏幕阅读器里那些图是什么样的

做前端第八年,我第一次认真戴上耳机、关掉屏幕、用 VoiceOver 从头到尾"听"完自己做的页面,是被一个客服工单逼的。有个视障用户投诉说我们商品列表页"根本没法用,每张图都念一串乱码"。我打开一看——好家伙,产品图的 alt 全是后端拼的文件名,alt="IMG_20240517_093412_v2_final_final.jpg"。屏幕阅读器一板一眼地把这串东西读了出来。那一刻我意识到,我过去写的无数个alt属性,基本都是应付 lint 规则用的,从没考虑过它真正被念出来是什么感受。

这篇就把我这两年趟出来的图片可访问性经验倒一倒。不讲大道理,全是能直接抄进项目的判断和代码。

先分清:这张图到底是干嘛的

所有关于 alt 的纠结,本质都是一个问题没想清楚——这张图在页面里承担的是"信息"还是"装饰"。这两类的处理方式是相反的,混了就出错。

信息性图片,指的是图片本身携带内容,去掉它信息就缺了一块。产品照片、图表、说明性截图、有文字的 banner,都算。这类必须写有意义的 alt。

装饰性图片,指的是它纯粹是视觉点缀,去掉它内容一点不少。分隔线、圆角背景纹理、纯氛围的配图、和旁边文字重复的图标,都算。这类必须显式声明为空,让屏幕阅读器直接跳过。

关键的坑在装饰这一侧。很多人以为装饰图不写 alt 就行了,错。看这段:

<!-- 灾难写法:屏幕阅读器会念出 URL 或文件名 --><imgsrc="/assets/divider-gold.png"><!-- 正确:空 alt,屏幕阅读器完全跳过 --><imgsrc="/assets/divider-gold.png"alt="">

<img>没有 alt 属性时,多数屏幕阅读器会退而去读src的文件名当替代文本,于是用户听到一串divider-gold-png。而alt=""(注意是空字符串,中间不能有空格)是一个明确信号:这张图无内容价值,别念。这俩差了十万八千里,我 review 代码现在第一件事就是抓没有 alt 属性的<img>

如果你更想表达语义上"这是个装饰角色",也可以用role="presentation"role="none",效果和空 alt 类似,都是把元素从无障碍树里剔掉:

<imgsrc="/bg-texture.png"alt=""role="presentation">

不过实测下来,alt=""兼容性最稳,能覆盖到就够了,role是锦上添花。

信息性图片的 alt,写"内容"不写"外观"

好,装饰的搞定了,难的是信息图 alt 怎么写。我总结的一条准则:替代文本要传达图片承载的信息和功能,而不是描述像素长什么样。

举个例子,同一张"红色感叹号图标 + 库存不足"的图:

<!-- 描述外观,没用 --><imgsrc="/icon-warning.svg"alt="红色三角形里有个感叹号"><!-- 传达信息,对了 --><imgsrc="/icon-warning.svg"alt="库存不足">

用户不需要知道那是三角形还是圆形、红色还是橙色——那是给视觉设计看的。他需要知道的是"库存不足"这件事。

再进阶一层,alt 要看上下文。同一张爱因斯坦的照片,放在人物传记页和放在"经典打印海报"电商页,alt 该完全不同。前者可能是"阿尔伯特·爱因斯坦晚年肖像",后者更该是"爱因斯坦吐舌头黑白海报,A2 尺寸",因为在购物场景里用户关心的是这个商品是什么。同一张图没有标准答案,脱离上下文谈 alt 都是耍流氓。

几条我常用的实操细节:

  • alt 别加"图片:“” 一张……的照片"这种前缀。屏幕阅读器碰到<img>本来就会先播报"图像",你再写"图片"就成了"图像 图片 XXX",啰嗦。
  • 如果图片是个链接或按钮里唯一的内容,alt 必须描述这个操作会去哪/做什么,而不是描述图长啥样。比如站标 logo 通常链到首页,写alt="返回首页"alt="公司logo"有用得多。
  • 图里有文字的话,alt 一般要把关键文字抄进去,尤其是营销图上的价格、优惠信息。这些文字对视觉用户可见,对视障用户就凭空消失了。
  • 长度别失控。一句话讲清就行,通常别超过一行。真需要长描述的(复杂图表),用下面讲的aria-describedby

复杂图表和信息图:一句 alt 装不下

数据可视化是重灾区。一张趋势折线图,你 alt 写"2024 年销售趋势图"等于没写——用户知道有张图,但图里说了啥一无所知。

处理思路有两层。第一层,alt 给个概括结论。第二层,用aria-describedby挂一段详细的文字替代,把图里的数据讲清楚。

<figure><imgsrc="/sales-2024.svg"alt="2024 年季度销售额呈上升趋势"aria-describedby="sales-desc"><figcaptionid="sales-desc">Q1 销售额 120 万元,Q2 增至 165 万元,Q3 达到 210 万元, Q4 冲到 280 万元。全年环比持续增长,Q4 增幅最大, 较 Q3 提升约 33%。</figcaption></figure>

这里我把详细描述放进了可见的<figcaption>,一举两得:视觉用户也能看到数据摘要,视障用户通过aria-describedby关联读到。aria-describedby的值是目标元素的 id,屏幕阅读器读完 alt 会接着读这段。

早年有个longdesc属性专门干这事,但它是外链一个描述页面的,浏览器支持烂、维护也烦,现在基本淘汰了。aria-describedby加可见 figcaption 是我现在的默认解法。如果你不想让描述在视觉上显示,可以放进一个视觉隐藏但屏幕阅读器可读的容器(下面有.sr-only的写法),但我更倾向让所有人都看到数据摘要,反正对谁都有好处。

信息图(infographic)同理,那种一大张塞满流程和数字的图,最好在旁边或下方提供等价的结构化文本/列表,别指望一个 alt 交代清楚。

SVG 图标的可访问性,比 img 麻烦

内联 SVG 现在满天飞,但它不是<img>,没有 alt 属性,处理方式单独一套。而且不同屏幕阅读器对 SVG 的支持参差不齐,这块坑最多。

先分场景。

纯装饰的 SVG 图标(旁边已经有文字标签的那种),直接aria-hidden="true"藏掉:

<button><svgaria-hidden="true"focusable="false"width="16"height="16"><usehref="#icon-cart"></use></svg>加入购物车</button>

aria-hidden="true"把 SVG 从无障碍树移除,用户只会听到按钮的文字"加入购物车"。那个focusable="false"是给老 IE/Edge 擦屁股的——某些旧版本里 SVG 会意外可聚焦,Tab 键会卡在上面,加上这个防患。现在能不能省?看你兼容目标,我一般还是留着,一个属性的事。

承载信息的 SVG(比如一个只有图标没有文字的图标按钮),必须补语义。两种主流写法:

<!-- 方式一:aria-label 直接给可访问名 --><buttonaria-label="搜索"><svgaria-hidden="true"focusable="false"><usehref="#icon-search"/></svg></button><!-- 方式二:SVG 内部用 title + role --><svgrole="img"aria-labelledby="search-title"width="20"height="20"><titleid="search-title">搜索</title><usehref="#icon-search"/></svg>

方式一是我用得最多的:图标 SVG 一律aria-hidden,把可访问名交给外层的button/aaria-label提供。逻辑清晰,也不受 SVG 屏幕阅读器兼容性折腾。

方式二用 SVG 内部的<title>role="img"aria-labelledby。注意光有<title>不够可靠,Safari+VoiceOver 有时不认,所以要显式role="img"aria-labelledby指向 title 的 id,把三件套配齐兼容性才稳。SVG 里还有个<desc>可以放更长的描述,用得少,复杂图形才需要。

顺便提一句<use href="#...">引用 symbol 的写法,图标本体的 symbol 定义里也别塞 title,容易和使用处的语义打架,语义统一在使用处给。

CSS 背景图:无障碍树里根本不存在

这个概念很多人没意识到:用background-image铺的图,屏幕阅读器完全感知不到,无障碍树里它不存在。

这既是坑也是特性。

如果你拿背景图去放装饰性内容——纹理、渐变、氛围图——那太好了,天生就被无障碍忽略,你啥也不用管,这正是我们想要的。

但如果你偷懒把信息性内容做成了背景图(我见过有人把"限时五折"的促销文字直接切成背景图铺上去),那视障用户就彻底丢失了这个信息,因为背景图没有任何替代文本机制。这种情况要么改回<img alt>,要么在那个元素上补 ARIA:

<divclass="hero-banner"role="img"aria-label="限时五折促销,仅剩 3 天"></div>

给背景图容器加role="img"aria-label,相当于手动把它塞回无障碍树。但说实话这是补救,能用真<img>就用真的,语义天然、SEO 也友好。

真机实测:VoiceOver 和 NVDA 会打架

代码写得再漂亮,不上真机听一遍都是纸上谈兵。我踩过最多的亏就是"看起来 ARIA 都对,实际读出来不对"。几个实测经验:

macOS VoiceOver(Cmd+F5 开启,Ctrl+Option+右方向键逐项走):对 SVG<title>的支持时好时坏,尤其配 Safari。前面说的role="img"aria-labelledby三件套就是为了绕它。VoiceOver 读<img>会先播"图像"再读 alt。

Windows NVDA(免费,配 Firefox 或 Chrome 测):整体对 ARIA 支持更规矩,但它和浏览器的组合会影响结果——NVDA+Firefox 和 NVDA+Chrome 对同一段aria-describedby的播报时机能不一样。所以我至少测 VoiceOver/Safari + NVDA/Firefox 两组。

一个特别容易翻车的点:aria-label和元素内的可见文字同时存在时,aria-label覆盖掉可见文字,屏幕阅读器只念 aria-label。我见过有人给一个"加入购物车"按钮加了aria-label="购物车",结果视障用户听到的是"购物车",把动作词"加入"给吞了。所以按钮如果本身有文字,多半根本不需要 aria-label,别画蛇添足。

还有个免费的低成本自查手段:装个 axe DevTools 或用 Lighthouse 的 accessibility 审计跑一遍,能自动抓出缺 alt、对比度不足这类硬伤。但记住,自动化工具只能查约 30%-40% 的问题,"alt 写得对不对、有没有意义"这种它判断不了,最后还得靠人戴上耳机真听。

一段能到处用的视觉隐藏工具类

最后送个高频工具。有时你想给屏幕阅读器提供额外文字(比如图标按钮的说明),又不想视觉上显示,别用display:nonevisibility:hidden——那俩会把内容从无障碍树也一起干掉,屏幕阅读器读不到。要用这个经典的.sr-only

.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
<ahref="/cart"><svgaria-hidden="true"><usehref="#icon-cart"/></svg><spanclass="sr-only">购物车,3 件商品</span></a>

它把元素缩到 1px 挤出视口外、裁剪掉,视觉上不可见,但仍留在无障碍树里,屏幕阅读器照读。这一个类能解决大半"想补语义又不想动视觉"的场景。

说点诚实的边界

写到这儿得泼点冷水。图片可访问性没有银弹,alt的"正确"高度依赖上下文和业务判断,很多时候没有唯一答案,只有更合适的答案——这活儿本质上是内容工作,不是纯技术活,指望某个规则或工具帮你自动生成好 alt,目前都不现实。自动生成的描述往往只描述像素、抓不住页面语境,还得人来兜底改。

另外我这套经验主要覆盖屏幕阅读器这一类用户,可访问性远不止于此——低视力放大、色盲对比度、键盘导航、认知障碍,每一块都是深水区,我也还在学。SVG 那部分的兼容性尤其是流沙,浏览器和读屏软件版本一变可能就有出入,别把我上面写的当成永久真理,上你自己的目标环境实测才算数。

真要开始改,我的建议是别追求一步到位。先跑个 axe 把没有 alt 的图全揪出来堵上(这是最低标准,也是收益最高的一步),再逐步区分装饰与信息、给关键图表补文字替代。哪怕只做到第一步,对那些戴着耳机"听"你页面的用户来说,已经是天壤之别了。图片是网页里最容易被无障碍遗忘的角落,但它其实是最好补的一块——很多时候,就差一个认真写的alt