小程序无障碍与适老化改造

小程序无障碍与适老化改造的完整实操指南:梳理 aria-role、aria-label 等无障碍属性的语义用法与小程序特有差异,讲解屏幕阅读器朗读顺序、焦点管理与动态播报的适配细节,给出字号缩放、弹性布局与触达区域的适老化改造方案,并附 WCAG 对比度校验公式与真机验收清单。

一个 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:13:1
AAA(增强)7:14.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 倍字号的真机测试。这些改造不会让年轻用户感到任何不便,却决定了相当一部分用户能否用得了你的小程序。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「miniprogram」更多文章

  1. 小程序第三方 SDK 集成与治理
  2. 小程序架构演进与遗留重构
  3. 小程序深色模式与主题系统