可访问的锚点平滑滚动:prefers-reduced-motion 与焦点管理

举报
yd_246123679 发表于 2026/10/30 00:16:06 2026/10/30
【摘要】 同页锚点若直接平滑滚动,可能与减少动态效果偏好冲突,也会让键盘用户失去位置感。本文用 prefers-reduced-motion 媒体查询切换滚动行为,并在点击后把焦点移到目标标题、按需设置 tabindex='-1',给出可运行示例与边界说明。

可访问的锚点平滑滚动:prefers-reduced-motion 与焦点管理

问题场景

同页目录常用锚点链接。默认点击会立即跳转,部分站点用 scroll-behavior: smooth 让滚动更连贯。但这带来两个可访问性问题:一是动画没有尊重用户的“减少动态效果”偏好;二是视觉虽然滚到目标,键盘焦点仍停留在导航链接上,后续按 Tab 会从原位置继续,键盘用户容易迷失。本文给出一套最小实现:CSS 负责声明滚动行为,JS 负责锚点点击后的焦点转移,并在减少动态效果时禁用平滑动画。

实现思路

  1. 在 CSS 中为根滚动容器开启平滑滚动,同时用 @media (prefers-reduced-motion: reduce) 把 scroll-behavior 改回 auto。
  2. 拦截同文档锚点链接的点击,避免默认跳转与后续焦点不一致。
  3. 目标若是标题,给它设置 tabindex=-1(若原本没有),使其可被脚本聚焦但不进入 Tab 顺序;调用 focus({ preventScroll: true }) 把焦点移过去。
  4. 根据 matchMedia('(prefers-reduced-motion: reduce)') 的当前值选择 scrollIntoView 的 behavior:减少动态效果时用 auto,否则用 smooth。最后用 history.pushState 更新地址栏 hash,不触发额外滚动。
  5. 页面加载时若已有 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' 会受 CSS scroll-behavior 影响。因此示例在减少动态效果的媒体查询中把根元素设为 auto,确保即时滚动。
  • 固定页头可能遮住目标标题,可用 scroll-padding-top 或 scroll-margin-top 留出空间。示例用了占位值 1rem。
  • 有多个滚动容器时,scroll-behavior 应设置在对应滚动容器上;scrollIntoView 会滚动最近的可滚动祖先,但焦点管理仍需指向具体目标。
  • 不要拦截带修饰键、中键或 target 非 _self 的链接。示例已过滤这些情况,避免影响新标签页打开等原生行为。
  • history.pushState 更新 hash 不触发默认滚动,但浏览器前进/后退时可能自行恢复滚动位置。示例只在 popstate 中恢复焦点,不强制再次滚动,以免和浏览器的位置恢复冲突。

小结

把“滚动动画”和“焦点位置”分开处理:CSS 用媒体查询尊重减少动态效果,JS 在锚点点击后把焦点交给目标标题,并只在允许动画时使用平滑滚动。这样既保留视觉连贯性,也让键盘用户知道页面当前定位在哪里。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。