本文约 6,315 字,预计阅读 16 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:你的主题,键盘用户可能根本用不了
试一个动作:打开你的站点,只用 Tab 键,能走到主导航吗?能看到当前焦点在哪吗?能打开二级菜单吗?很多主题在这里就露馅了——焦点框被 CSS 的 outline: none 去掉了,鼠标悬停才展开的二级菜单用键盘永远打不开,读屏软件读到的是一堆没有语义的 div。
可访问性(Accessibility,简称 a11y)在国内常被当成「无障碍验收」时才补的加分项,但它的影响面远比想象中大:政府与企业站点的合规要求、Google 的核心网页指标与 SEO 评分、以及最朴素的——老年用户、键盘用户、使用读屏软件的用户能不能正常用你的站。WordPress 官方主题目录里就有 accessibility-ready 这个标签,达标并不难,难的是从一开始就别把基础做错。
原理分析:三个层次的要求
可访问性落到主题代码上,可以拆成三层:
| 层次 | 要求 | 典型实现 |
|---|---|---|
| 语义 | 用正确的标签表达结构 | <nav>、<main>、<header>、<footer>、<button> |
| 可达 | 键盘能操作、焦点可见 | 不删 outline、跳转链接、正确的 tab 顺序 |
| 可读 | 读屏软件能理解 | aria-label、<label>、图片的 alt、ARIA 状态 |
几个高频概念要分清:role="navigation" 是给读屏的语义补充,但原生 <nav> 已经隐含这个角色,不必重复加;aria-label 用于给无文字的按钮(如汉堡菜单图标)提供名称;.screen-reader-text(Underscores 主题里的经典类)是「视觉隐藏但读屏可读」的实现,跳转链接就靠它;wp.a11y.speak() 是 WordPress 提供的 JS 方法,用于动态内容变化时向读屏播报。
颜色对比度也是硬指标:正文文字与背景的对比度至少 4.5:1,大字号(18pt 以上或 14pt 粗体)可放宽到 3:1。很多浅灰配白的「高级感」配色在这一点上直接不合格。
保姆级实操
第一步:语义骨架
<?php
/**
* header.php 片段
*/
?>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<a class="skip-link screen-reader-text" href="#primary"><?php esc_html_e( '跳到主要内容', 'mimangfei' ); ?></a>
<header id="masthead" class="site-header">
<nav id="site-navigation" class="main-navigation" aria-label="<?php esc_attr_e( '主导航', 'mimangfei' ); ?>">
<?php
wp_nav_menu( array(
'theme_location' => 'primary',
'menu_id' => 'primary-menu',
'container' => false,
) );
?>
</nav>
</header>
<main id="primary" class="site-main">
要点:wp_body_open()(WP 5.2+)必须紧跟在 <body> 后,否则插件注入的内容会错位;<main> 一篇页面只能有一个;wp_nav_menu 外面包 <nav> 并加 aria-label,不要依赖 container 生成的 div。
第二步:跳转链接的样式与聚焦
.screen-reader-text {
border: 0;
clip-path: inset( 50% );
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}
.skip-link:focus,
.screen-reader-text:focus {
clip-path: none;
display: block;
top: 0;
left: 0;
z-index: 100000;
width: auto;
height: auto;
padding: 12px 20px;
background: #1a1a1a;
color: #fff;
text-decoration: none;
}
跳转链接必须是 <body> 后第一个可聚焦元素,且 :focus 时要可见——隐藏但不显示焦点,等于没有。
第三步:焦点样式不要删
/* 错误示范:键盘用户将彻底失去方向感 */
/* *:focus { outline: none; } */
/* 正确做法:只在鼠标点击时隐藏,键盘导航时保留 */
:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
a:focus,
button:focus,
input:focus,
textarea:focus,
select:focus {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
:focus-visible 已被现代浏览器广泛支持,能区分「键盘聚焦」与「鼠标点击」,不用再写 JS hack。
第四步:菜单按钮与二级菜单的键盘可达
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => false,
'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
'fallback_cb' => false,
) );
按钮用 <button> 而不是 <a href="#">:
<button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false">
<?php esc_html_e( '菜单', 'mimangfei' ); ?>
</button>
配套的 JS 在展开/收起时同步更新 aria-expanded,并在 Esc 键时关闭菜单、把焦点还给按钮:
const toggle = document.querySelector( '.menu-toggle' );
const menu = document.getElementById( 'primary-menu' );
toggle.setAttribute( 'aria-expanded', 'false' );
toggle.addEventListener( 'click', () => {
const expanded = toggle.getAttribute( 'aria-expanded' ) === 'true';
toggle.setAttribute( 'aria-expanded', String( ! expanded ) );
menu.classList.toggle( 'toggled' );
} );
document.addEventListener( 'keydown', ( e ) => {
if ( e.key === 'Escape' && toggle.getAttribute( 'aria-expanded' ) === 'true' ) {
toggle.setAttribute( 'aria-expanded', 'false' );
menu.classList.remove( 'toggled' );
toggle.focus();
}
} );
二级菜单项还需支持键盘展开(focus 时也显示子菜单),CSS 里除了 :hover 要加 :focus-within:
.main-navigation ul ul { display: none; }
.main-navigation li:hover > ul,
.main-navigation li:focus-within > ul { display: block; }
第五步:表单、图片与分页
<label for="mf-search-field"><?php esc_html_e( '搜索', 'mimangfei' ); ?></label>
<input type="search" id="mf-search-field" class="search-field" name="s" value="<?php echo esc_attr( get_search_query() ); ?>">
<button type="submit"><span class="screen-reader-text"><?php esc_html_e( '提交搜索', 'mimangfei' ); ?></span></button>
if ( has_post_thumbnail() ) {
the_post_thumbnail( 'large', array(
'alt' => the_title_attribute( array( 'echo' => false ) ),
) );
}
分页用核心函数,它自带 aria-label 与 nav 语义:
the_posts_pagination( array(
'mid_size' => 2,
'prev_text' => __( '上一页', 'mimangfei' ),
'next_text' => __( '下一页', 'mimangfei' ),
) );
不要用裸的 paginate_links() 拼 HTML,会丢掉 aria-current="page" 这类状态标记。
第六步:动态播报与对比度自检
wp.a11y.speak( '文章已加载完成', 'polite' );
前提是把 wp-a11y 加入依赖:
wp_enqueue_script(
'mimangfei-app',
get_template_directory_uri() . '/assets/js/app.js',
array( 'wp-a11y' ),
wp_get_theme()->get( 'Version' ),
true
);
对比度用浏览器 DevTools 的「颜色选择器」直接看 Contrast Ratio,或用 axe DevTools 插件跑一遍全站扫描。
常见踩坑
outline: none一刀切:最常见的错误,键盘用户完全失去焦点指示。- 用
<div onclick>当按钮:读屏读不出它是按钮,键盘也无法聚焦,必须换成<button>。 - 图标按钮无 aria-label:只有放大镜图标的搜索按钮,读屏播报为「按钮」,用户不知道是干什么的。
- 跳转链接指向的 id 不存在:
href="#content"但<main>的 id 是#primary,等于白做。 - 装饰图片写了 alt:纯装饰图应
alt="",否则读屏会念出一堆无用信息;反过来内容图不写 alt 又是另一类错误。 - 案例:某企业站改版后收到无障碍投诉:键盘 Tab 走到页脚时会「消失」。原因是页脚的「返回顶部」用了
<a href="#">,且 CSS 给所有a:focus设了outline: none,用户 Tab 到那里既看不到焦点也无法触发。修复改成<button>加aria-label,并恢复焦点样式,问题一次性解决。 aria-label与可见文本冲突:按钮文字是「搜索」而aria-label写「Submit」,读屏用户听到的与看到的对不上,会被判为不合规。title属性当替代文本:title在触屏和多数读屏下不可靠,alt与aria-label才是正确做法。
总结
一份能直接执行的自查清单:<body> 后第一个元素是可聚焦的跳转链接;全站只有一个 <main>;导航用 <nav> 且带 aria-label;交互元素全是 <button> / <a>;:focus-visible 样式存在且清晰;表单都有 <label>;图片 alt 有意义;正文对比度 ≥ 4.5:1;只用 Tab 能走完首页到页脚并提交一次搜索。这十项过了,主题的 a11y 就已经超过大多数同类。若想申请官方目录的 accessibility-ready 标签,还要把读屏软件(NVDA 或 VoiceOver)跑一遍完整流程,并把 主题上线前自检 与 主题打包发布规范 一起走完。
