可访问的锚点平滑滚动:prefers-reduced-motion 与焦点管理
【摘要】 同页锚点若直接平滑滚动,可能与减少动态效果偏好冲突,也会让键盘用户失去位置感。本文用 prefers-reduced-motion 媒体查询切换滚动行为,并在点击后把焦点移到目标标题、按需设置 tabindex='-1',给出可运行示例与边界说明。
可访问的锚点平滑滚动:prefers-reduced-motion 与焦点管理
问题场景
同页目录常用锚点链接。默认点击会立即跳转,部分站点用 scroll-behavior: smooth 让滚动更连贯。但这带来两个可访问性问题:一是动画没有尊重用户的“减少动态效果”偏好;二是视觉虽然滚到目标,键盘焦点仍停留在导航链接上,后续按 Tab 会从原位置继续,键盘用户容易迷失。本文给出一套最小实现:CSS 负责声明滚动行为,JS 负责锚点点击后的焦点转移,并在减少动态效果时禁用平滑动画。
实现思路
- 在 CSS 中为根滚动容器开启平滑滚动,同时用
@media (prefers-reduced-motion: reduce)把scroll-behavior改回auto。 - 拦截同文档锚点链接的点击,避免默认跳转与后续焦点不一致。
- 目标若是标题,给它设置
tabindex=-1(若原本没有),使其可被脚本聚焦但不进入 Tab 顺序;调用focus({ preventScroll: true })把焦点移过去。 - 根据
matchMedia('(prefers-reduced-motion: reduce)')的当前值选择scrollIntoView的behavior:减少动态效果时用auto,否则用smooth。最后用history.pushState更新地址栏 hash,不触发额外滚动。 - 页面加载时若已有 hash,也把焦点移到对应目标,避免初始定位后焦点仍在文档开头。
可运行的最小示例
将下面内容保存为 anchor-a11y.html,用浏览器打开。导航链接指向同页两个标题。示例中的 id 和文案都是占位值,可替换成实际内容。
<!doctype html>
<html lang='zh-CN'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>锚点可访问滚动示例</title>
<style>
html { scroll-behavior: smooth; scroll-padding-top: 1rem; }
body { font-family: system-ui, sans-serif; line-height: 1.7; margin: 2rem; }
nav a { margin-right: 1rem; }
section { min-height: 60vh; padding-top: 1rem; }
h2:focus { outline: 2px solid #2b6cb0; outline-offset: 0.25rem; }
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
}
</style>
</head>
<body>
<nav aria-label='示例目录'>
<a href='#section-demo'>跳到示例标题</a>
<a href='#section-usage'>跳到使用说明</a>
</nav>
<section>
<h2 id='section-demo'>示例标题</h2>
<p>这里是第一段占位内容。</p>
</section>
<section>
<h2 id='section-usage'>使用说明</h2>
<p>这里是第二段占位内容。</p>
</section>
<script>
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
function focusTarget(target) {
if (!target) return;
if (!target.hasAttribute('tabindex')) {
target.setAttribute('tabindex', '-1');
}
try {
target.focus({ preventScroll: true });
} catch (error) {
target.focus();
}
}
function getHashTarget() {
const hash = window.location.hash;
if (!hash || hash === '#') return null;
let id;
try {
id = decodeURIComponent(hash.slice(1));
} catch (error) {
return null;
}
return document.getElementById(id);
}
document.addEventListener('click', (event) => {
if (event.defaultPrevented || event.button !== 0) return;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const link = event.target.closest('a[href^="#"]');
if (!link) return;
if (link.target && link.target !== '_self') return;
const hash = link.getAttribute('href');
if (!hash || hash === '#') return;
let id;
try {
id = decodeURIComponent(hash.slice(1));
} catch (error) {
return;
}
const target = document.getElementById(id);
if (!target) return;
event.preventDefault();
focusTarget(target);
target.scrollIntoView({
behavior: reduceMotion.matches ? 'auto' : 'smooth',
block: 'start'
});
history.pushState(null, '', hash);
});
window.addEventListener('load', () => {
focusTarget(getHashTarget());
});
window.addEventListener('popstate', () => {
focusTarget(getHashTarget());
});
</script>
</body>
</html>
运行与验证:打开页面后,用 Tab 键把焦点移到第一个导航链接,按 Enter。视觉应滚动到“示例标题”,document.activeElement 应指向对应标题;再按 Tab,焦点从标题之后继续。开启系统的“减少动态效果”后刷新页面,重复操作,滚动应为即时而非动画。也可以在开发者工具中模拟 prefers-reduced-motion(若工具支持)。注意,不同浏览器与辅助技术的焦点轮廓表现可能不同,测试时以实际目标环境为准。
边界与限制
- 示例只拦截同文档
href='#...'链接。跨页面锚点、由脚本直接改location.hash的跳转,需要在目标页加载后另行处理焦点。 prefers-reduced-motion是用户偏好,用户可能随时修改。示例在点击时读取.matches,没有假设它永远不变。focus({ preventScroll: true })在部分旧环境中可能不可用或退化;如果先发生立即滚动,后续平滑滚动可能被跳过。这是可接受的降级,但上线前应测试目标环境。tabindex=-1使目标可编程聚焦且不进入 Tab 顺序。若目标本身已有tabindex,不要覆盖;示例只在属性缺失时添加,并保留它,避免焦点在移除属性后丢失。scrollIntoView的behavior: 'auto'会受 CSSscroll-behavior影响。因此示例在减少动态效果的媒体查询中把根元素设为auto,确保即时滚动。- 固定页头可能遮住目标标题,可用
scroll-padding-top或scroll-margin-top留出空间。示例用了占位值1rem。 - 有多个滚动容器时,
scroll-behavior应设置在对应滚动容器上;scrollIntoView会滚动最近的可滚动祖先,但焦点管理仍需指向具体目标。 - 不要拦截带修饰键、中键或
target非_self的链接。示例已过滤这些情况,避免影响新标签页打开等原生行为。 history.pushState更新 hash 不触发默认滚动,但浏览器前进/后退时可能自行恢复滚动位置。示例只在popstate中恢复焦点,不强制再次滚动,以免和浏览器的位置恢复冲突。
小结
把“滚动动画”和“焦点位置”分开处理:CSS 用媒体查询尊重减少动态效果,JS 在锚点点击后把焦点交给目标标题,并只在允许动画时使用平滑滚动。这样既保留视觉连贯性,也让键盘用户知道页面当前定位在哪里。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)