mcpField
A
accessible-animation

accessible-animation

github.com/iart-ai设计与 UI632.7k 次安装18 周前更新
代码仓库

关于 accessible-animation

用于创建和审查网页动画的设计技能,强调支持减少动态效果偏好。它将动画分为移除、弱化和保留三类,并提供 CSS、JavaScript 模式及截图验证流程。

主要功能

  1. 01减少动态效果的三级规则
  2. 02CSS 媒体查询模式
  3. 03JavaScript matchMedia 控制
  4. 04React 减少动态效果 Hook
  5. 05独立演示验证

适用场景

  1. 01审查可能引发不适的动画
  2. 02调整 GSAP 或 Framer Motion 效果
  3. 03制作支持减少动态效果的网页演示

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/iart-ai/web-animation-skills --skill accessible-animation

安装后在 Claude Code 中输入/accessible-animation确认已加载。

附带文件README.mdreferences/patterns.md

详细介绍

无障碍动画(减少动态效果,分级处理)

将 prefers-reduced-motion: reduce 视为一种优雅降级,而非一键关闭。操作系统级设置(macOS:Settings > Accessibility > Display > Reduce motion;Windows:Settings > Accessibility > Visual effects > Animation effects;iOS/Android 中的对应设置)表示,可能引发前庭不适的动态效果会导致恶心、眩晕或偏头痛。正确做法是移除有害的动态效果,同时保留方向提示。

适用场景

在网页 UI 中添加或审查动画、用户报告晕动症或 WCAG 审查发现问题,或在无障碍产品中集成动画库(GSAP、Framer Motion、Lenis smooth scroll、Anime.js、CSS keyframes)时使用。

核心原则:动态效果分级处理,不要全部关闭

移除所有动画是常见的过度修正。没有任何过渡的即时状态变化可能更令人迷失方向(元素仿佛瞬间传送),也会去掉实用的提示(焦点环、用于传达进度的加载动画)。将每种动画归入以下三个级别:

  • 第 1 级 — 完全移除(前庭刺激因素)。 视差;在视口范围内大幅滑动或平移;大型元素的缩放;3D 旋转和转动;持续自动播放的轮播和跑马灯;劫持滚动的平滑滚动(Lenis/Locomotive);由滚动劫持的固定场景;运动路径动画;任何会让屏幕大范围移动或产生深度感/自身运动感的效果。
  • 第 2 级 — 弱化/缩短。 改为短暂的透明度淡入淡出(≤200ms)。减小移动距离和持续时间。保留过渡这一事实(避免元素瞬间出现),但去掉位移效果。将 600ms 的从左侧滑入改为 150ms 的交叉淡化。
  • 第 3 级 — 始终保留。 透明度淡入淡出、颜色/背景过渡、焦点环过渡、加载指示器,以及对表达含义至关重要的动态效果(WCAG 允许必要的动画)。这些效果通常不会引发前庭反应,因为它们不会产生自身运动感。

CSS 层:使用媒体查询进行控制

默认启用动态效果,然后在减少动态效果的查询条件下覆盖。优先采用全局安全网,再针对具体效果进行覆盖。

/* Global safety net: neutralize runaway motion but DO NOT set 0s blindly,
   which can break JS that waits for transitionend/animationend. Use 0.01ms. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;   /* stop infinite spins/marquees */
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;          /* kill smooth scroll */
  }
}

全局安全网只是兜底措施。在此基础上叠加有意设置、分级处理的覆盖规则,以便保留第 3 级动态效果:

.card {
  transition: transform 400ms ease, opacity 400ms ease, background-color 200ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .card {
    /* Tier 2: drop the transform (movement), keep opacity + color (Tier 3) */
    transition: opacity 150ms ease, background-color 150ms ease;
  }
  .parallax-layer { transform: none !important; }        /* Tier 1: remove */
  .hero-zoom      { animation: none !important; }        /* Tier 1: remove */
}

对于复杂效果,使用正向查询 (prefers-reduced-motion: no-preference) 来选择性启用动态效果,这是最安全的模式:

.hero { opacity: 1; } /* visible, static by default */
@media (prefers-reduced-motion: no-preference) {
  .hero { animation: zoom-in 1.2s ease both; } /* only animate when allowed */
}

JS 层:使用 matchMedia 进行控制

CSS 无法控制由 JS 驱动的动画(GSAP 时间轴、Framer 的 animate、canvas/WebGL、Lenis)。通过 matchMedia 读取相同的信号,并响应实时变化(用户可以在不重新加载页面的情况下切换操作系统设置)。

const REDUCE_QUERY = '(prefers-reduced-motion: reduce)';

export function prefersReducedMotion() {
  return typeof window !== 'undefined'
    && window.matchMedia
    && window.matchMedia(REDUCE_QUERY).matches;
}

// Live updates: listen so toggling the OS setting takes effect immediately.
export function onReducedMotionChange(cb) {
  const mq = window.matchMedia(REDUCE_QUERY);
  const handler = (e) => cb(e.matches);
  mq.addEventListener('change', handler);          // modern API
  return () => mq.removeEventListener('change', handler);
}
在库中统一添加控制层,以便复用
// GSAP — use matchMedia(): GSAP reverts conditional setups on change.
import gsap from 'gsap';
const mm = gsap.matchMedia();
mm.add({
  motionOK: '(prefers-reduced-motion: no-preference)',
  motionReduce: '(prefers-reduced-motion: reduce)',
}, (ctx) => {
  const { motionOK } = ctx.conditions;
  if (motionOK) {
    gsap.from('.hero', { y: 80, opacity: 0, duration: 1 });   // Tier 1 move
  } else {
    gsap.from('.hero', { opacity: 0, duration: 0.15 });        // Tier 2 fade
  }
});

// Lenis smooth scroll — do not instantiate at all when reduced.
import Lenis from 'lenis';
let lenis = null;
if (!prefersReducedMotion()) {
  lenis = new Lenis();
  const raf = (t) => { lenis.raf(t); requestAnimationFrame(raf); };
  requestAnimationFrame(raf);
}

React:useReducedMotion Hook

import { useState, useEffect } from 'react';

export function useReducedMotion() {
  const query = '(prefers-reduced-motion: reduce)';
  const get = () =>
    typeof window !== 'undefined' && window.matchMedia
      ? window.matchMedia(query).matches
      : false;
  const [reduced, setReduced] = useState(get);

  useEffect(() => {
    const mq = window.matchMedia(query);
    const onChange = () => setReduced(mq.matches);
    onChange();                                   // sync after hydration
    mq.addEventListener('change', onChange);
    return () => mq.removeEventListener('change', onChange);
  }, []);

  return reduced;
}
// Framer Motion has its own useReducedMotion(); the hook above also works.
import { motion } from 'framer-motion';
function Card() {
  const reduced = useReducedMotion();
  return (
    <motion.div
      initial={reduced ? { opacity: 0 } : { opacity: 0, y: 40 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: reduced ? 0.15 : 0.5 }}
    />
  );
}

将 SSR 渲染状态初始化为 false(启用动态效果),并在 useEffect 中重新同步,以避免 hydration 不匹配;只有在客户端才能获取操作系统设置值。

WCAG 对应条款

  • 2.3.3 交互触发的动画(AAA)。 由交互(滚动、悬停)触发的动态效果必须可以禁用,除非它们是必要的。遵循 prefers-reduced-motion 是标准做法。
  • C39(CSS 技术)。 使用 prefers-reduced-motion 查询来阻止动态效果。这是指定的充分技术。
  • 2.2.2 暂停、停止、隐藏(A)。 任何持续时间超过 5s 的自动播放动态效果都必须提供暂停/停止功能。无论媒体查询如何设置,自动轮播和跑马灯都不符合要求,因此也要提供控制选项。

交付并验证(独立 HTML)

打包的辅助脚本(scripts/):scripts/seek-shot.sh anim.html 0 1.5 3 会冻结 ?t=N 测试代码,并为每个时刻截图;scripts/contact-sheet.sh sheet.png frame-*.png 会将截图拼成一张图,方便快速查看。请参阅 scripts/README.md。

对于自包含演示,交付物应是一个可直接在浏览器中打开的 HTML 文件——标记、CSS 和任何 JS(来自 CDN 的 GSAP/matchMedia)都内联,不需要构建步骤。对于本技能,验证是重点:必须同时为常规路径和减少动态效果路径截图,并确认分级正确。

输出要求:

  • 一个 .html 文件,包含完整动态效果和分级的减少动态效果覆盖规则(CSS 查询,以及适用于 JS 驱动动态效果的 matchMedia/gsap.matchMedia())。
  • 一个用于冻结画面的定位测试代码,以及一种在截图时强制走减少动态效果代码路径的方法。

定位测试代码——冻结画面,强制指定偏好。 ?t=N 会冻结时间线;即使操作系统设置关闭,?reduce=1 也会强制走减少动态效果路径(CSS 无法通过 JS 切换,因此应查询用于控制 JS 的同一标志,并在为 CSS 层模拟操作系统/浏览器偏好后截图):

<script>
  const q = new URLSearchParams(location.search);
  const reduce = q.get("reduce") === "1"
    || window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  // build motion conditionally on `reduce` (Tier-1 removed, Tier-2 softened, Tier-3 kept)
  const t = q.get("t");
  if (t !== null) { tl.pause(); tl.seek(parseFloat(t)); }      // GSAP; CSS: el.style.animationDelay=(-t)+"s"; playState="paused"
  window.__ready = true;
</script>

**验证流程——为两种状态都截图:**分别捕获常规路径和减少动态效果路径,并确保每种状态都处于动画中段;确认 Tier-1 动态效果已移除、Tier-2 动态效果是短暂淡入淡出、Tier-3 动态效果保留。为 CSS 层模拟偏好(Playwright 使用 --reduced-motion=reduce;Agent 的浏览器工具也可以模拟):

npx playwright screenshot --wait-for-timeout=500 "file://$PWD/demo.html?t=0.6" normal.png
npx playwright screenshot --reduced-motion=reduce --wait-for-timeout=500 "file://$PWD/demo.html?t=0.6&reduce=1" reduced.png

完成前:

  1. 可独立打开——没有控制台错误,CDN(如果有)能正常加载。
  2. 冻结功能 + ?reduce=1(以及 --reduced-motion=reduce)能可靠地触发两种代码路径。
  3. 两种状态都在动画中段完成截图——常规状态效果自然,减少动态效果状态会移除 Tier-1、减弱 Tier-2。
  4. prefers-reduced-motion 应按分级处理,而非作为总开关——不得瞬移;保留 Tier-3 淡入淡出、焦点环和加载动画;任何超过 5 秒的自动动态效果仍须提供暂停控件(2.2.2)。
  5. 两种路径的缓动都经过合理设计——没有意外的 linear,时长合理(减少动态效果时淡入淡出 ≤200ms;使用 0.01ms 而非 0s)。

快速参考

动态效果类型层级减少动态效果时的处理
视差/景深1移除(transform: none)
大幅滑动/平移1移除或替换为淡入淡出
大幅缩放/放大1移除
旋转/3D 旋转1移除(animation: none)
自动轮播/跑马灯1停止并提供暂停控件(2.2.2)
平滑滚动(Lenis)1不要实例化
小幅滑动/弹入2交叉淡入淡出 ≤200ms
透明度淡入淡出3保留(如果耗时较长则缩短)
颜色/焦点环过渡3保留
加载动画3保留(必要的反馈)

注意事项

  • animation-duration: 0s 可能会导致等待 animationend/transitionend 的代码出错;请使用 0.01ms,确保事件仍会触发。
  • 仅使用全局 * 重置会过度移除 Tier 3 动态效果;务必添加有针对性的覆盖规则。
  • 忘记设置 animation-iteration-count: 1 会使无限旋转动画以接近零的时长继续运行(造成 CPU 空转,动画仍在进行)。
  • 媒体查询无法覆盖 JS/canvas/WebGL;应使用 matchMedia 控制,否则循环永远不会停止。
  • 切换操作系统设置不会触发页面重新加载;如果没有 change 监听器,页面会继续使用过时状态。
  • SSR:在渲染期间读取 matchMedia 会报错或导致不匹配;默认启用动态效果,并在 effect 中同步。
  • 即使关闭减少动态效果,自动播放的动态效果仍须提供可见的暂停控件(2.2.2 属于 A 级)。

参考文件

  • references/patterns.md — 完整的 GSAP matchMedia 清理、Framer Motion MotionConfig、实时切换时启动/停止 Lenis、可复用的 animateWithMotionPreference 封装、带暂停控件的跑马灯,以及动态效果分级审查清单。

评论

支持 Markdown