一个 65 岁的用户打开你的小程序,发现字号太小、按钮点不中、验证码看不清;一个视障用户打开同样的页面,屏幕阅读器念出一串「按钮 按钮 按钮」,完全无法知道每个按钮是干什么的。这两类问题本质上是同一个:界面只对「视力正常、操作精准的年轻用户」可用。
无障碍(Accessibility,简称 a11y)与适老化是两个高度重叠的需求——适老化改造中的字号放大、对比度提升、触达区域扩大,同时也是无障碍的基本要求。工信部《互联网网站适老化通用设计规范》与 WCAG 2.1 在核心指标上高度一致。本文把两件事放在一起讲,因为它们共用同一套技术手段和验收标准。
一、小程序无障碍的能力底座
小程序提供了一组 aria-* 属性,会被系统无障碍服务(iOS VoiceOver / Android TalkBack)识别。它们写在 WXML 标签上,语法与 Web 的 ARIA 基本一致。
| 属性 | 作用 | 典型取值 |
|---|---|---|
aria-role | 声明元素角色 | button / link / img / heading |
aria-label | 提供可读名称 | 任意描述性文本 |
aria-hidden | 从无障碍树中隐藏 | true / false |
aria-checked | 开关/选中状态 | true / false |
aria-disabled | 禁用状态 | true / false |
aria-live | 动态内容播报 | polite / assertive |
<view class="toolbar">
<!-- 纯图标按钮:必须补 aria-label,否则阅读器只念「按钮」 -->
<button class="toolbar__btn" aria-role="button" aria-label="返回上一页" bindtap="goBack">
<image src="/assets/icon-back.png" aria-hidden="true" />
</button>
<button class="toolbar__btn" aria-role="button" aria-label="分享给好友" bindtap="onShare">
<image src="/assets/icon-share.png" aria-hidden="true" />
</button>
</view>
关键点:图标本身要 aria-hidden="true",名称由外层容器提供。如果两者都设,阅读器会念出重复内容。
1.1 与 Web ARIA 的差异
小程序不是浏览器,ARIA 支持并不完整:
- 不支持
role属性(必须用aria-role) - 不支持
tabindex,焦点顺序由文档流决定 aria-live的支持度有限,部分版本只在assertive下生效- 自定义组件需要在组件根节点显式声明无障碍属性
这意味着不能直接照搬 Web 的无障碍方案,必须真机验证。Web 侧成熟的无障碍测试方法与工具链(自动化扫描、人工走查、辅助技术组合测试)在 无障碍测试 中有完整梳理,可以据此裁剪出适合小程序的验证流程。
二、屏幕阅读器适配
2.1 语义化优先
在补 aria-label 之前,先检查能不能用语义化标签。小程序内置组件 <button>、<checkbox>、<switch>、<slider> 自带角色与状态语义,比 <view> 加一堆 aria 属性可靠得多。
<!-- 差:view 模拟按钮,阅读器识别不出可点击 -->
<view class="btn" bindtap="submit">提交</view>
<!-- 好:原生 button,自带 role=button 与点击语义 -->
<button class="btn" bindtap="submit">提交</button>
2.2 图片的替代文本
所有承载信息的图片都必须有 aria-label;纯装饰图片用 aria-hidden="true" 排除。
<!-- 信息图:描述内容 -->
<image src="/assets/chart.png" aria-role="img" aria-label="2026 年第三季度销售额环比增长 23%" />
<!-- 装饰图:排除 -->
<image src="/assets/divider.png" aria-hidden="true" />
替代文本的写法有讲究:「图片」「图标」这类词不要写,要直接说清楚图片传达的信息。
2.3 动态内容播报
表单校验失败、加载完成、购物车数量变化这类动态反馈,需要用 aria-live 主动播报,否则视障用户完全不知道发生了什么。
<view aria-live="assertive" class="toast {{toastVisible ? 'toast--show' : ''}}">
{{toastMessage}}
</view>
Page({
submit() {
if (!this.data.phone) {
// 视觉提示 + 无障碍播报同时发生
this.setData({ toastMessage: '手机号不能为空', toastVisible: true })
return
}
}
})
2.4 状态同步
可切换的控件要把状态写进 ARIA,而不是只靠视觉样式:
<view
class="switch {{enabled ? 'switch--on' : ''}}"
aria-role="switch"
aria-checked="{{enabled}}"
aria-label="接收推送通知"
bindtap="toggle"
/>
阅读器会念出「接收推送通知,开关,已打开」,比视觉上的绿色高亮信息量大得多。
三、焦点管理与触达区域
3.1 焦点顺序
小程序没有 tabindex,焦点顺序严格等于 WXML 的文档顺序。这带来两个约束:
- 视觉上的顺序必须与 DOM 顺序一致,不能用 CSS 的
flex-direction: row-reverse之类的技巧把视觉顺序反过来 - 弹窗(Modal)出现时,底层内容的焦点无法用
tabindex屏蔽,只能用aria-hidden="true"临时隐藏
<view class="page-content" aria-hidden="{{modalVisible}}">
<!-- 弹窗打开时,整块内容从无障碍树移除 -->
</view>
<view class="modal" wx:if="{{modalVisible}}" aria-role="dialog" aria-label="确认订单">
<text>确认支付 99 元?</text>
<button bindtap="confirm">确认</button>
</view>
3.2 触达区域最小尺寸
WCAG 2.1 的 2.5.5 条款要求可点击目标至少 44×44 CSS 像素(iOS HIG 与 Material Design 均采用此值)。小程序的 rpx 在 iPhone 6 上 1rpx = 0.5px,所以 88rpx 才等于 44px。
.tap-target {
/* 视觉上可能是 60rpx 的小图标 */
width: 60rpx;
height: 60rpx;
/* 用 padding 把实际可点击区域撑到 88rpx */
padding: 14rpx;
box-sizing: content-box;
}
/* 或者用伪元素扩展热区,不影响布局 */
.icon-btn {
position: relative;
}
.icon-btn::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 88rpx;
height: 88rpx;
transform: translate(-50%, -50%);
}
列表项、Tab 项、关闭按钮、复选框是触达区域最容易踩线的地方。
3.3 焦点可见性
键盘或开关控制设备(Switch Control)操作时,必须有清晰的焦点指示。默认的 outline 在小程序里表现不稳定,建议自定义:
.focusable:focus {
outline: 4rpx solid var(--color-brand);
outline-offset: 4rpx;
}
四、色彩对比度
WCAG 2.1 对文本对比度有明确要求:
| 等级 | 普通文本 | 大文本(≥18pt 或 ≥14pt 粗体) |
|---|---|---|
| AA(最低要求) | 4.5:1 | 3:1 |
| AAA(增强) | 7:1 | 4.5:1 |
对比度(Contrast Ratio)的计算公式基于相对亮度:
// 相对亮度计算(WCAG 2.1 定义)
function luminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map(c => {
c = c / 255
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
})
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
}
function contrastRatio(hex1, hex2) {
const [l1, l2] = [hex1, hex2].map(hex => {
const n = parseInt(hex.slice(1), 16)
return luminance((n >> 16) & 255, (n >> 8) & 255, n & 255)
})
const [light, dark] = l1 > l2 ? [l1, l2] : [l2, l1]
return (light + 0.05) / (dark + 0.05)
}
// 例:正文色 #666666 在 #ffffff 上的对比度
console.log(contrastRatio('#666666', '#ffffff').toFixed(2)) // 5.74 —— 达标 AA
console.log(contrastRatio('#999999', '#ffffff').toFixed(2)) // 2.85 —— 不达标
最常见的坑是占位符(placeholder)与次要文字用了 #999999,在白色背景上只有 2.85:1,远低于 4.5:1。次要文字至少要用 #767676(4.54:1)。
配色方案的系统性设计(含对比度校验流程)可以参考 数据可视化中的无障碍配色 ,其中的调色板生成与自动校验方法同样适用于界面配色。
五、适老化改造
适老化不只是「把字放大」,而是一整套针对老年用户的可用性优化。
5.1 字号与缩放
微信客户端自身提供「关怀模式」(字体放大),小程序可以通过 wx.getSystemInfoSync().fontSizeSetting 读取用户设定的字号缩放比例:
Page({
onLoad() {
const { fontSizeSetting, windowWidth } = wx.getSystemInfoSync()
// fontSizeSetting 默认 16,用户放大后可达 20 以上
const scale = fontSizeSetting / 16
this.setData({ fontScale: Math.min(scale, 1.5) })
}
})
.text-body {
font-size: calc(32rpx * var(--font-scale, 1));
line-height: 1.6;
}
<view class="page" style="--font-scale: {{fontScale}}">
设计上的要点:
- 不要用固定高度容器装文本,字号放大后会截断。用
min-height或让内容撑开 - 行高用无单位倍数(如
1.6),不要用32rpx这类绝对值,否则字号放大后行距不会跟着放大 - 避免多列布局,老年用户更适应单列纵向滚动
5.2 弹性布局
适老化改造会打破「设计师按标准字号排好的版」,因此布局必须能容忍文本变长:
.list-item {
display: flex;
align-items: center;
min-height: 96rpx; /* 不是 height */
padding: 24rpx 32rpx;
}
.list-item__label {
flex: 1;
min-width: 0; /* 关键:允许 flex 子项收缩 */
word-break: break-word;
}
min-width: 0 是弹性布局里最容易被忽略的一行——没有它,长文本会把 flex 子项撑爆而不是换行。
5.3 交互简化
- 减少需要精确操作的控件(滑块、拖拽、长按),改为点击
- 关键操作提供二次确认,防止误触
- 表单减少必填项,提供大字号输入键盘(
<input>的type与confirm-type) - 加载状态要有明确文字提示,不要只用转圈动画
5.4 与国际化改造的协同
字号缩放与多语言文本长度变化,本质上都是「文本长度不可控」的问题。/miniprogram-internationalization/ 里讲的弹性布局与文本溢出处理,与适老化改造可以共用同一套布局规范——两者都应该要求「布局对文本长度不敏感」。这套规范最终应沉淀到 /miniprogram-design-system/ 的组件约束里,而不是每个页面各自处理。
六、测试与验收
6.1 真机测试方法
| 平台 | 开启方式 | 关注点 |
|---|---|---|
| iOS | 设置 → 辅助功能 → 旁白 | 朗读顺序、可点击元素识别 |
| iOS | 设置 → 辅助功能 → 显示与文字大小 → 更大字体 | 布局是否截断 |
| Android | 设置 → 无障碍 → TalkBack | 中文朗读准确性 |
| 微信 | 设置 → 关怀模式 | 全局字号放大后的表现 |
开发者工具的模拟器无法验证无障碍,必须真机。建议在每个迭代周期的验收环节固定跑一遍核心流程(登录、下单、支付)的旁白朗读。
6.2 验收清单
- 所有可点击元素都能被旁白识别,且名称有意义
- 图标按钮都有
aria-label,装饰图标aria-hidden="true" - 弹窗打开时底层内容被
aria-hidden屏蔽 - 表单错误有
aria-live播报 - 所有文本对比度 ≥ 4.5:1(大文本 ≥ 3:1)
- 可点击区域 ≥ 88rpx × 88rpx
- 字号放大到 1.5 倍后无内容截断、无横向滚动
- 列表项、按钮在放大后不重叠
6.3 自动化检查
对比度可以脚本化校验。把设计令牌(见深色模式一文中的 tokens.json)与文本尺寸组合,在 CI 里跑一遍对比度计算,低于阈值的直接失败。这比逐页人工检查高效得多。
小结
无障碍与适老化改造在技术上高度重合,核心是三件事:让机器能读懂界面(ARIA 语义、状态同步、动态播报)、让操作不需要精准(触达区域、简化交互、二次确认)、让内容能适应变化(字号缩放、弹性布局、对比度)。
落地建议:从语义化标签开始,把 <view> 模拟的按钮全部换回原生组件;然后系统性地补 aria-label 与状态属性;接着把对比度和触达区域做成 CI 校验;最后专门跑一轮 1.5 倍字号的真机测试。这些改造不会让年轻用户感到任何不便,却决定了相当一部分用户能否用得了你的小程序。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。