pin_drop当前位置:知识文库 ❯ 图文
HTML表单:HTML表单无障碍详解 - ARIA属性与WCAG标准
教程简介
Web 无障碍(Accessibility,简称 A11y)确保所有用户,包括视觉障碍、听觉障碍、运动障碍和认知障碍的用户,都能平等地访问和使用网页内容。表单是用户与网站交互的核心界面,其无障碍性直接影响大量用户能否完成注册、购买、搜索等关键操作。
本教程将详细介绍 HTML 表单的无障碍访问实践,包括 ARIA 属性的使用、错误提示的无障碍设计、键盘导航、屏幕阅读器兼容性,以及 WCAG 标准中的表单相关要求。
核心概念
WCAG 表单相关原则
WCAG(Web Content Accessibility Guidelines)为表单无障碍提供了四项原则:
ARIA 属性概述
ARIA(Accessible Rich Internet Applications)为 HTML 元素提供了额外的语义信息,帮助辅助技术理解页面结构和交互状态。
语法与用法
常用 ARIA 属性详解
aria-label
代码示例
<!-- 为无可见标签的控件提供标签 -->
<button type="button" aria-label="关闭对话框">×</button>
<input type="search" aria-label="搜索" placeholder="搜索...">aria-labelledby
代码示例
<!-- 使用其他元素作为标签 -->
<div id="shipping-title">收货地址</div>
<input type="text" aria-labelledby="shipping-title">aria-describedby
代码示例
<!-- 关联帮助文本 -->
<label for="password">密码:</label>
<input type="password" id="password" aria-describedby="pwd-help">
<small id="pwd-help">密码至少8个字符,包含大小写字母和数字</small>aria-required 与 aria-invalid
代码示例
<!-- 必填和验证状态 -->
<input type="text" name="email" required
aria-required="true"
aria-invalid="false">aria-live
代码示例
<!-- 动态更新的区域 -->
<div aria-live="polite" id="search-results">
<!-- 搜索结果动态更新时,屏幕阅读器会朗读 -->
</div>
<div aria-live="assertive" role="alert" id="error-msg">
<!-- 紧急消息,屏幕阅读器立即朗读 -->
</div>aria-live 值说明
代码示例
示例 1:无障碍表单基础
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>无障碍表单基础</title>
<style>
body { font-family: "Microsoft YaHei", sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #f5f7fa; }
.card { background: #fff; padding: 28px; border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,0.08); }
h1 { color: #1a1a2e; margin-top: 0; }
.form-group { margin-bottom: 18px; }
label { display: block; margin-bottom: 6px; font-weight: 600; color: #333; font-size: 14px; }
.required-mark { color: #e74c3c; margin-left: 2px; }
input, select, textarea { width: 100%; padding: 10px 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; font-family: inherit; transition: border-color 0.2s; }
input:focus, select:focus, textarea:focus { outline: none; border-color: #4a90d9; box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.15); }
.help-text { display: block; margin-top: 4px; font-size: 12px; color: #888; }
.error-text { display: block; margin-top: 4px; font-size: 12px; color: #e74c3c; }
input[aria-invalid="true"] { border-color: #e74c3c; }
button { padding: 12px 28px; background: #4a90d9; color: #fff; border: none; border-radius: 6px; font-size: 15px; font-weight: 600; cursor: pointer; }
button:hover { background: #3a7bc8; }
button:focus-visible { outline: 3px solid #4a90d9; outline-offset: 2px; }
.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; }
</style>
</head>
<body>
<div class="card">
<h1>无障碍注册表单</h1>
<form id="a11yForm" novalidate>
<div class="form-group">
<label for="fullname">姓名<span class="required-mark" aria-hidden="true">*</span></label>
<input type="text" id="fullname" name="fullname" required aria-required="true"
aria-describedby="fullname-help" autocomplete="name">
<small id="fullname-help" class="help-text">请输入您的真实姓名</small>
</div>
<div class="form-group">
<label for="a11y-email">邮箱<span class="required-mark" aria-hidden="true">*</span></label>
<input type="email" id="a11y-email" name="email" required aria-required="true"
aria-describedby="email-help email-error" autocomplete="email">
<small id="email-help" class="help-text">请输入有效的邮箱地址</small>
<small id="email-error" class="error-text" role="alert" aria-live="assertive"></small>
</div>
<div class="form-group">
<label for="a11y-phone">手机号<span class="required-mark" aria-hidden="true">*</span></label>
<input type="tel" id="a11y-phone" name="phone" required aria-required="true"
pattern="^1[3-9]\d{9}$" aria-describedby="phone-help phone-error" autocomplete="tel">
<small id="phone-help" class="help-text">请输入11位手机号码</small>
<small id="phone-error" class="error-text" role="alert" aria-live="assertive"></small>
</div>
<div class="form-group">
<label for="a11y-pwd">密码<span class="required-mark" aria-hidden="true">*</span></label>
<input type="password" id="a11y-pwd" name="password" required aria-required="true"
minlength="8" aria-describedby="pwd-help" autocomplete="new-password">
<small id="pwd-help" class="help-text">密码至少8个字符,包含大小写字母和数字</small>
</div>
<div class="form-group">
<label for="a11y-bio">个人简介:</label>
<textarea id="a11y-bio" name="bio" rows="3" aria-describedby="bio-help" maxlength="200"></textarea>
<small id="bio-help" class="help-text">最多200个字符(选填)</small>
</div>
<button type="submit">注册</button>
</form>
</div>
<script>
const form = document.getElementById('a11yForm');
function validateField(input) {
const errorId = input.id.replace('a11y-', '') + '-error';
const errorEl = document.getElementById(errorId);
if (!errorEl) return input.checkValidity();
if (!input.checkValidity()) {
let message = '';
if (input.validity.valueMissing) { message = '此字段为必填项'; }
else if (input.validity.typeMismatch) { message = '请输入有效的格式'; }
else if (input.validity.patternMismatch) { message = '请输入有效的手机号码'; }
errorEl.textContent = message;
input.setAttribute('aria-invalid', 'true');
input.setAttribute('aria-errormessage', errorId);
return false;
} else {
errorEl.textContent = '';
input.removeAttribute('aria-invalid');
input.removeAttribute('aria-errormessage');
return true;
}
}
form.querySelectorAll('input').forEach(input => {
input.addEventListener('blur', function() { if (this.value) validateField(this); });
input.addEventListener('input', function() {
if (this.getAttribute('aria-invalid') === 'true') { validateField(this); }
});
});
form.addEventListener('submit', function(e) {
e.preventDefault();
let firstInvalid = null;
this.querySelectorAll('input[required]').forEach(input => {
if (!validateField(input) && !firstInvalid) { firstInvalid = input; }
});
if (firstInvalid) { firstInvalid.focus(); }
else { alert('注册成功!'); }
});
</script>
</body>
</html>示例 2:ARIA 属性应用
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ARIA 属性应用</title>
<style>
body { font-family: "Microsoft YaHei", sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #f5f7fa; }
.card { background: #fff; padding: 24px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); margin-bottom: 20px; }
h2 { margin-top: 0; color: #2c3e50; font-size: 18px; }
.form-group { margin-bottom: 16px; }
label { display: block; margin-bottom: 6px; font-weight: 600; font-size: 14px; color: #333; }
input, select { width: 100%; padding: 10px 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; }
input:focus, select:focus { outline: none; border-color: #9b59b6; box-shadow: 0 0 0 3px rgba(155, 89, 182, 0.15); }
.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; }
.search-wrapper { position: relative; }
.search-wrapper input { padding-right: 40px; }
.search-btn { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); background: none; border: none; font-size: 18px; cursor: pointer; padding: 6px 10px; color: #9b59b6; }
.search-btn:focus-visible { outline: 2px solid #9b59b6; border-radius: 4px; }
.radio-group { display: flex; gap: 16px; }
.radio-group label { display: flex; align-items: center; gap: 6px; font-weight: 400; cursor: pointer; }
.tag-group { display: flex; flex-wrap: wrap; gap: 8px; }
.tag-label { display: flex; align-items: center; gap: 6px; padding: 6px 14px; border: 2px solid #e0e0e0; border-radius: 20px; cursor: pointer; font-size: 14px; transition: all 0.2s; }
.tag-label:has(input:checked) { border-color: #9b59b6; background: #f3e5f5; }
.tag-label input { display: none; }
.tag-label:focus-within { box-shadow: 0 0 0 3px rgba(155, 89, 182, 0.2); }
.status-msg { margin-top: 12px; padding: 10px; border-radius: 6px; font-size: 14px; }
.status-info { background: #e8f4fd; color: #2c5f8a; }
button[type="submit"] { padding: 10px 24px; background: #9b59b6; color: #fff; border: none; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; }
button:hover { background: #8e44ad; }
</style>
</head>
<body>
<h1>ARIA 属性应用</h1>
<!-- aria-label:无可见标签的控件 -->
<div class="card">
<h2>aria-label - 不可见标签</h2>
<div class="search-wrapper">
<label for="search2" class="sr-only">搜索文章</label>
<input type="search" id="search2" name="search" aria-label="搜索文章" placeholder="搜索文章...">
<button type="button" class="search-btn" aria-label="执行搜索">🔍</button>
</div>
</div>
<!-- aria-labelledby:用其他元素作为标签 -->
<div class="card">
<h2>aria-labelledby - 引用标签</h2>
<p id="shipping-label">请填写您的收货地址</p>
<div class="form-group">
<input type="text" name="address" aria-labelledby="shipping-label" placeholder="详细地址">
</div>
</div>
<!-- aria-describedby:关联帮助文本 -->
<div class="card">
<h2>aria-describedby - 关联描述</h2>
<div class="form-group">
<label for="username3">用户名:</label>
<input type="text" id="username3" name="username"
aria-describedby="username-format username-availability">
<small id="username-format">格式:3-20个字母、数字或下划线</small>
<small id="username-availability" aria-live="polite"></small>
</div>
</div>
<!-- fieldset/legend + radio -->
<div class="card">
<h2>fieldset/legend - 分组单选</h2>
<fieldset>
<legend>通知偏好:</legend>
<div class="radio-group">
<label><input type="radio" name="notify" value="all" checked> 全部通知</label>
<label><input type="radio" name="notify" value="important"> 仅重要通知</label>
<label><input type="radio" name="notify" value="none"> 关闭通知</label>
</div>
</fieldset>
</div>
<!-- 复选框组 -->
<div class="card">
<h2>复选框组 - 角色标签</h2>
<fieldset>
<legend>选择兴趣标签:</legend>
<div class="tag-group" role="group" aria-label="兴趣标签">
<label class="tag-label"><input type="checkbox" name="tags" value="tech"> 技术</label>
<label class="tag-label"><input type="checkbox" name="tags" value="design"> 设计</label>
<label class="tag-label"><input type="checkbox" name="tags" value="product"> 产品</label>
<label class="tag-label"><input type="checkbox" name="tags" value="data"> 数据</label>
<label class="tag-label"><input type="checkbox" name="tags" value="ai"> AI</label>
</div>
</fieldset>
</div>
<!-- aria-live:动态内容 -->
<div class="card">
<h2>aria-live - 动态消息</h2>
<div class="form-group">
<label for="city2">输入城市:</label>
<input type="text" id="city2" name="city" placeholder="输入城市名">
</div>
<div id="city-status" class="status-msg status-info" aria-live="polite">等待输入...</div>
</div>
<script>
const cityInput = document.getElementById('city2');
const cityStatus = document.getElementById('city-status');
cityInput.addEventListener('input', function() {
if (this.value) { cityStatus.textContent = '正在搜索:' + this.value + '...'; }
else { cityStatus.textContent = '等待输入...'; }
});
</script>
</body>
</html>示例 3:无障碍错误提示
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>无障碍错误提示</title>
<style>
body { font-family: "Microsoft YaHei", sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #f5f7fa; }
.card { background: #fff; padding: 28px; border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,0.08); }
h1 { color: #1a1a2e; margin-top: 0; }
.form-group { margin-bottom: 18px; }
label { display: block; margin-bottom: 6px; font-weight: 600; color: #333; font-size: 14px; }
.required-mark { color: #e74c3c; }
input { width: 100%; padding: 10px 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; }
input:focus { outline: none; border-color: #4a90d9; box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.15); }
input[aria-invalid="true"] { border-color: #e74c3c; box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.1); }
.error-msg { display: flex; align-items: center; gap: 6px; margin-top: 6px; font-size: 13px; color: #e74c3c; }
.error-msg .icon { font-size: 16px; }
.help-text { margin-top: 4px; font-size: 12px; color: #888; }
.error-summary { margin-bottom: 20px; padding: 16px; background: #fde8e8; border: 2px solid #e74c3c; border-radius: 8px; }
.error-summary h2 { margin: 0 0 8px 0; color: #e74c3c; font-size: 16px; }
.error-summary ul { margin: 0; padding-left: 20px; }
.error-summary li { margin-bottom: 4px; font-size: 14px; }
.error-summary a { color: #c0392b; text-decoration: underline; }
button { padding: 12px 28px; background: #4a90d9; color: #fff; border: none; border-radius: 6px; font-size: 15px; font-weight: 600; cursor: pointer; }
button:hover { background: #3a7bc8; }
</style>
</head>
<body>
<div class="card">
<h1>无障碍错误提示</h1>
<!-- 错误汇总区域 -->
<div class="error-summary" id="errorSummary" role="alert" aria-live="assertive" style="display:none">
<h2>表单存在以下错误:</h2>
<ul id="errorList"></ul>
</div>
<form id="errorForm" novalidate>
<div class="form-group">
<label for="name4">姓名<span class="required-mark" aria-hidden="true">*</span></label>
<input type="text" id="name4" name="name" required aria-required="true"
aria-describedby="name-help name-error">
<small id="name-help" class="help-text">请输入您的姓名</small>
<div id="name-error" class="error-msg" role="alert" aria-live="assertive"></div>
</div>
<div class="form-group">
<label for="email4">邮箱<span class="required-mark" aria-hidden="true">*</span></label>
<input type="email" id="email4" name="email" required aria-required="true"
aria-describedby="email-help email-error">
<small id="email-help" class="help-text">请输入有效的邮箱地址</small>
<div id="email-error" class="error-msg" role="alert" aria-live="assertive"></div>
</div>
<div class="form-group">
<label for="pwd4">密码<span class="required-mark" aria-hidden="true">*</span></label>
<input type="password" id="pwd4" name="password" required aria-required="true"
minlength="8" aria-describedby="pwd-help pwd-error">
<small id="pwd-help" class="help-text">至少8个字符</small>
<div id="pwd-error" class="error-msg" role="alert" aria-live="assertive"></div>
</div>
<button type="submit">提交</button>
</form>
</div>
<script>
const form = document.getElementById('errorForm');
const errorSummary = document.getElementById('errorSummary');
const errorList = document.getElementById('errorList');
const validationRules = {
name4: { required: true, messages: { valueMissing: '请输入您的姓名' } },
email4: { required: true, type: 'email', messages: { valueMissing: '请输入邮箱地址', typeMismatch: '请输入有效的邮箱格式' } },
pwd4: { required: true, minLength: 8, messages: { valueMissing: '请输入密码', tooShort: '密码至少需要8个字符' } }
};
function getErrorMessage(input) {
const rules = validationRules[input.id];
if (!rules) return '';
const validity = input.validity;
const messages = rules.messages;
if (validity.valueMissing) return messages.valueMissing || '此字段为必填项';
if (validity.typeMismatch) return messages.typeMismatch || '格式不正确';
if (validity.tooShort) return messages.tooShort || '输入内容太短';
return input.validationMessage;
}
function validateField(input) {
const errorId = input.id.replace('4', '') + '-error';
const errorEl = document.getElementById(errorId);
if (!input.checkValidity()) {
const message = getErrorMessage(input);
errorEl.innerHTML = '<span class="icon" aria-hidden="true">⚠</span> ' + message;
input.setAttribute('aria-invalid', 'true');
input.setAttribute('aria-errormessage', errorId);
return { field: input, label: input.previousElementSibling?.textContent || input.name, message };
} else {
errorEl.innerHTML = '';
input.removeAttribute('aria-invalid');
input.removeAttribute('aria-errormessage');
return null;
}
}
form.addEventListener('submit', function(e) {
e.preventDefault();
const errors = [];
this.querySelectorAll('input[required]').forEach(input => {
const error = validateField(input);
if (error) errors.push(error);
});
if (errors.length > 0) {
errorList.innerHTML = errors.map(err =>
'<li><a href="#' + err.field.id + '">' + err.label + '</a>:' + err.message + '</li>'
).join('');
errorSummary.style.display = 'block';
errorSummary.focus();
errors[0].field.focus();
} else {
errorSummary.style.display = 'none';
alert('表单提交成功!');
}
});
form.querySelectorAll('input').forEach(input => {
input.addEventListener('input', function() {
if (this.getAttribute('aria-invalid') === 'true') { validateField(this); }
});
});
</script>
</body>
</html>浏览器兼容性
注意事项与最佳实践
1. 每个表单控件都必须有可访问的名称
代码示例
<!-- 方式一:label 元素(推荐) -->
<label for="email">邮箱:</label>
<input type="email" id="email" name="email">
<!-- 方式二:aria-label -->
<input type="search" aria-label="搜索文章">
<!-- 方式三:aria-labelledby -->
<h2 id="section-title">联系方式</h2>
<input type="text" aria-labelledby="section-title">
<!-- 错误:无任何标签 -->
<input type="text" name="email" placeholder="邮箱">2. 使用原生 HTML 元素而非 ARIA 模拟
代码示例
<!-- 推荐:使用原生元素 -->
<button type="submit">提交</button>
<input type="checkbox" id="agree" name="agree">
<label for="agree">同意</label>
<!-- 不推荐:用 ARIA 模拟原生元素 -->
<div role="button" tabindex="0" onclick="submit()">提交</div>
<div role="checkbox" aria-checked="false" tabindex="0">同意</div>第一规则:如果原生 HTML 元素能实现所需语义和行为,就不要使用 ARIA。
3. 错误提示必须可被感知
代码示例
<!-- 推荐:错误提示关联到控件 -->
<input type="email" id="email" aria-describedby="email-error" aria-invalid="true">
<div id="email-error" role="alert">请输入有效的邮箱地址</div>
<!-- 不推荐:错误提示无法被屏幕阅读器识别 -->
<input type="email" id="email">
<div class="error" style="color:red;">请输入有效的邮箱地址</div>4. 键盘可操作性
代码示例
<!-- 所有控件必须可通过键盘访问 -->
<!-- Tab 键:在控件间导航 -->
<!-- Enter/Space:激活按钮/复选框 -->
<!-- 方向键:在单选组/选项间切换 -->
<!-- 确保自定义组件支持键盘操作 -->
<div role="listbox" tabindex="0" aria-label="选择颜色"
onkeydown="handleKeyDown(event)">
<div role="option" aria-selected="true">红色</div>
<div role="option">蓝色</div>
</div>5. 焦点管理
代码示例
// 表单验证失败后聚焦到第一个错误字段
form.addEventListener('submit', function(e) {
if (!this.checkValidity()) {
e.preventDefault();
const firstInvalid = this.querySelector(':invalid');
if (firstInvalid) {
firstInvalid.focus();
}
}
});
// 动态内容加载后聚焦到新内容
function loadStep(stepNumber) {
// 加载步骤内容...
const firstInput = document.querySelector('#step' + stepNumber + ' input');
if (firstInput) {
firstInput.focus();
}
}6. 必填标识的双重提示
代码示例
<!-- 视觉标识 + ARIA 标识 -->
<label for="email">
邮箱<span class="required-mark" aria-hidden="true">*</span>
</label>
<input type="email" id="email" name="email"
required aria-required="true">
<!-- aria-hidden="true" 隐藏星号,因为 aria-required 已提供语义 -->
<!-- 屏幕阅读器会朗读"邮箱,必填" -->代码规范示例
规范写法
代码示例
<!-- 规范:完整无障碍标记 -->
<form action="/api/register" method="post">
<fieldset>
<legend>注册信息</legend>
<div class="form-group">
<label for="reg-email">
邮箱<span class="required-mark" aria-hidden="true">*</span>
</label>
<input type="email" id="reg-email" name="email"
required
aria-required="true"
aria-describedby="reg-email-help reg-email-error"
autocomplete="email">
<small id="reg-email-help" class="help-text">请输入有效的邮箱地址</small>
<div id="reg-email-error" class="error-msg" role="alert" aria-live="assertive"></div>
</div>
<button type="submit">注册</button>
</fieldset>
</form>不规范写法
代码示例
<!-- 不规范:缺少 label、无 ARIA、错误不可感知 -->
<form>
<input type="email" name="email" placeholder="邮箱">
<div class="error" style="color:red;display:none;">邮箱格式错误</div>
<div class="submit-btn" onclick="submit()">提交</div>
</form>常见问题与解决方案
问题 1:placeholder 能否替代 label
代码示例
<!-- 不能!placeholder 不是 label 的替代品 -->
<!-- 问题:输入内容后 placeholder 消失,用户失去上下文 -->
<!-- 问题:屏幕阅读器对 placeholder 的支持不一致 -->
<!-- 错误 -->
<input type="text" name="email" placeholder="邮箱地址">
<!-- 正确 -->
<label for="email" class="sr-only">邮箱地址</label>
<input type="email" id="email" name="email" placeholder="邮箱地址">问题 2:如何处理动态表单验证
代码示例
<!-- 使用 aria-live 通知验证结果 -->
<input type="text" id="username" name="username"
aria-describedby="username-status">
<div id="username-status" aria-live="polite"></div>
<script>
const statusEl = document.getElementById('username-status');
usernameInput.addEventListener('input', async function() {
if (this.value.length < 3) {
statusEl.textContent = '用户名至少3个字符';
this.setAttribute('aria-invalid', 'true');
} else {
// 异步检查
const available = await checkUsername(this.value);
if (available) {
statusEl.textContent = '用户名可用';
this.removeAttribute('aria-invalid');
} else {
statusEl.textContent = '用户名已被占用';
this.setAttribute('aria-invalid', 'true');
}
}
});
</script>问题 3:自定义组件如何实现无障碍
代码示例
<!-- 自定义下拉选择框的无障碍实现 -->
<div class="custom-select" role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="select-label">
<button type="button" id="select-label"
aria-expanded="false">
选择颜色
</button>
<ul role="listbox" aria-label="颜色选项" hidden>
<li role="option" aria-selected="true">红色</li>
<li role="option">蓝色</li>
<li role="option">绿色</li>
</ul>
</div>
<!-- 必须支持的键盘操作 -->
<!-- Enter/Space:展开/收起列表 -->
<!-- 上下方向键:在选项间移动 -->
<!-- Enter:选择当前选项 -->
<!-- Escape:收起列表 -->问题 4:如何测试表单无障碍
代码示例
测试方法:
1. 键盘测试:仅用键盘完成整个表单填写和提交
2. 屏幕阅读器测试:使用 NVDA/JAWS/VoiceOver 测试
3. 缩放测试:将页面放大到 200%,检查布局是否正常
4. 高对比度测试:使用高对比度模式检查可见性
5. 自动化工具:使用 axe、Lighthouse 等工具扫描
6. Tab 顺序测试:确保 Tab 顺序符合逻辑问题 5:多步骤表单的无障碍处理
代码示例
<!-- 步骤指示器 -->
<nav aria-label="表单步骤">
<ol>
<li aria-current="step">
<span>步骤 1</span> 基本信息
</li>
<li>
<span>步骤 2</span> 联系方式
</li>
<li>
<span>步骤 3</span> 确认提交
</li>
</ol>
</nav>
<!-- 步骤切换时聚焦管理 -->
<script>
function goToStep(stepNumber) {
// 隐藏其他步骤,显示当前步骤
// ...
// 聚焦到步骤标题
const stepTitle = document.querySelector('#step' + stepNumber + ' h2');
stepTitle.setAttribute('tabindex', '-1');
stepTitle.focus();
// 通知屏幕阅读器
const announcer = document.getElementById('step-announcer');
announcer.textContent = '已进入步骤 ' + stepNumber;
}
</script>
<!-- 步骤切换通知区域 -->
<div id="step-announcer" class="sr-only" aria-live="polite"></div>总结
HTML 表单无障碍是确保所有用户都能平等使用表单的关键:
核心原则:无障碍不是附加功能,而是基本要求。从设计阶段就考虑无障碍,使用语义化 HTML 作为基础,仅在原生元素无法满足时才使用 ARIA 增强。定期使用键盘和屏幕阅读器测试表单,确保所有用户都能顺利完成操作。
小贴士
A11y 名称由来:Accessibility 这个单词共有 11 个字母,去掉首字母 A 和尾字母 y,中间恰好有 11 个字母,所以无障碍社区常用 "A11y" 作为简称。类似的缩写还有 i18n(internationalization)和 l10n(localization)。
常见问题
placeholder 可以替代 label 标签吗?
不能。placeholder 不是 label 的替代品。输入内容后 placeholder 会消失,用户会失去上下文;而且屏幕阅读器对 placeholder 的支持不一致。正确的做法是同时使用 label(可以用 sr-only 类隐藏视觉显示)和 placeholder 作为补充提示。
aria-label 和 aria-labelledby 有什么区别?
aria-label 直接提供标签文本,适用于没有可见标签的场景;aria-labelledby 引用页面上其他元素的 id 作为标签文本,适用于已有可见文字可以作为标签的场景。两者的优先级都高于原生 label 元素,当同时存在时,aria-labelledby 优先级最高。
如何确保表单对所有键盘用户可用?
确保所有表单控件可通过 Tab 键访问,按钮可通过 Enter/Space 激活,单选组可通过方向键切换。避免使用 div/span 模拟控件,优先使用原生 HTML 元素。如果需要自定义组件,必须添加 tabindex、role 和键盘事件处理。同时确保焦点样式清晰可见(不要移除 outline)。
什么情况下应该使用 ARIA?
ARIA 的第一规则是:如果原生 HTML 元素能实现所需语义和行为,就不要使用 ARIA。ARIA 应该在以下场景使用:1)原生元素无法满足语义需求时;2)需要补充额外的状态或属性信息时(如 aria-expanded、aria-invalid);3)动态内容更新需要通知屏幕阅读器时(aria-live)。
如何测试表单的无障碍性?
推荐测试方法:1)键盘测试——仅用键盘完成整个表单填写和提交;2)屏幕阅读器测试——使用 NVDA(Windows)、JAWS(Windows)或 VoiceOver(Mac);3)自动化工具——使用 axe、Lighthouse 等工具扫描;4)缩放测试——放大到 200% 检查布局;5)高对比度测试——使用高对比度模式检查可见性。
本文涉及AI创作
内容由AI创作,请仔细甄别