跳到主要内容

useReducedMotion

检测用户是否启用了"减少动画"偏好设置的 Hook。

基本用法

实时编辑器
function Demo() {
  const reducedMotion = useReducedMotion();

  return (
    <div
      style={{
        padding: '24px',
        backgroundColor: reducedMotion ? 'var(--ifm-color-warning-lightest)' : 'var(--ifm-color-success-lightest)',
        borderRadius: '8px',
        textAlign: 'center',
      }}
    >
      <div style={{ fontSize: '48px', marginBottom: '12px' }}>
        {reducedMotion ? '🐢' : '⚡'}
      </div>
      <div style={{ fontSize: '20px', fontWeight: 'bold', marginBottom: '8px' }}>
        {reducedMotion ? '减少动画模式' : '正常动画模式'}
      </div>
      <div style={{ fontSize: '14px', color: 'var(--ifm-color-emphasis-700)' }}>
        当前用户偏好: {reducedMotion ? '减少动画' : '允许动画'}
      </div>
    </div>
  );
}
结果
Loading...

条件动画

根据用户偏好启用或禁用动画:

实时编辑器
function Demo() {
  const reducedMotion = useReducedMotion();
  const [count, setCount] = useState(0);

  return (
    <div>
      <Button onClick={() => setCount(count + 1)} style={{ marginBottom: '16px' }}>
        点击增加 ({count})
      </Button>
      <div
        style={{
          padding: '40px',
          backgroundColor: 'var(--ifm-color-primary-lightest)',
          borderRadius: '8px',
          textAlign: 'center',
          transition: reducedMotion ? 'none' : 'all 0.5s ease',
          transform: `scale(${1 + count * 0.05})`,
        }}
      >
        <div style={{ fontSize: '48px' }}>🎯</div>
        <div style={{ marginTop: '12px', fontSize: '14px' }}>
          {reducedMotion ? '无动画缩放' : '平滑缩放动画'}
        </div>
      </div>
    </div>
  );
}
结果
Loading...

自适应过渡效果

动态调整过渡时间:

实时编辑器
function Demo() {
  const reducedMotion = useReducedMotion();
  const [visible, setVisible] = useState(true);

  const transitionDuration = reducedMotion ? '0s' : '0.3s';

  return (
    <div>
      <Button onClick={() => setVisible(!visible)} style={{ marginBottom: '12px' }}>
        {visible ? '隐藏' : '显示'}
      </Button>
      <div
        style={{
          height: visible ? '200px' : '0',
          opacity: visible ? 1 : 0,
          overflow: 'hidden',
          transition: `all ${transitionDuration}`,
          backgroundColor: 'var(--ifm-background-surface-color)',
          border: '1px solid var(--ifm-color-emphasis-300)',
          borderRadius: '8px',
        }}
      >
        <div style={{ padding: '20px' }}>
          <h4>内容区域</h4>
          <p style={{ marginBottom: 0 }}>
            这个区域会根据用户的动画偏好设置显示或隐藏。
            {reducedMotion ? '当前无过渡动画。' : '当前有平滑过渡动画。'}
          </p>
        </div>
      </div>
    </div>
  );
}
结果
Loading...

加载动画适配

为加载状态提供适配的动画效果:

实时编辑器
function Demo() {
  const reducedMotion = useReducedMotion();
  const [loading, setLoading] = useState(false);

  const handleLoad = () => {
    setLoading(true);
    setTimeout(() => setLoading(false), 2000);
  };

  return (
    <div>
      <Button onClick={handleLoad} disabled={loading} style={{ marginBottom: '16px' }}>
        {loading ? '加载中...' : '开始加载'}
      </Button>
      {loading && (
        <div
          style={{
            padding: '32px',
            backgroundColor: 'var(--ifm-color-emphasis-100)',
            borderRadius: '8px',
            textAlign: 'center',
          }}
        >
          <div
            style={{
              display: 'inline-block',
              width: '40px',
              height: '40px',
              border: '4px solid var(--ifm-color-primary-lightest)',
              borderTopColor: 'var(--ifm-color-primary)',
              borderRadius: '50%',
              animation: reducedMotion ? 'none' : 'spin 1s linear infinite',
            }}
          />
          <div style={{ marginTop: '12px', fontSize: '14px' }}>
            {reducedMotion ? '加载中(静态)' : '加载中(旋转动画)'}
          </div>
        </div>
      )}
      <style>{`
        @keyframes spin {
          to { transform: rotate(360deg); }
        }
      `}</style>
    </div>
  );
}
结果
Loading...

API

参数

function useReducedMotion(): boolean

无参数。

返回值

返回一个布尔值,表示用户是否启用了"减少动画"偏好。

boolean
  • true: 用户启用了减少动画
  • false: 用户允许正常动画

工作原理

通过媒体查询 prefers-reduced-motion 检测用户系统偏好设置:

@media (prefers-reduced-motion: reduce) {
/* 用户启用了减少动画 */
}

特性

  • 可访问性: 尊重用户的无障碍偏好
  • 自动响应: 用户更改设置时自动更新
  • 零配置: 无需额外配置即可使用
  • 性能友好: 对于启用减少动画的用户,可以提升性能

设置方法

macOS

系统偏好设置 → 辅助功能 → 显示 → 减少动态效果

Windows

设置 → 轻松使用 → 显示 → 在 Windows 中显示动画

iOS

设置 → 辅助功能 → 动态效果 → 减少动态效果

Android

设置 → 辅助功能 → 移除动画

使用场景

  • 过渡动画: 根据偏好禁用或减少过渡效果
  • 加载动画: 提供静态替代方案
  • 视差效果: 禁用可能引起不适的滚动效果
  • 自动播放: 停止自动轮播和视频播放
  • 悬停效果: 简化或移除复杂的悬停动画
  • 页面切换: 使用即时切换替代淡入淡出

最佳实践

渐进增强

const reducedMotion = useReducedMotion();

const animationProps = reducedMotion
? {}
: {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.3 },
};

return <motion.div {...animationProps}>内容</motion.div>;

CSS-in-JS 集成

const reducedMotion = useReducedMotion();

const styles = {
transition: reducedMotion ? 'none' : 'all 0.3s ease',
animation: reducedMotion ? 'none' : 'fadeIn 0.5s',
};

动画库配置

// Framer Motion
const reducedMotion = useReducedMotion();

<MotionConfig reducedMotion={reducedMotion ? 'always' : 'never'}>
{/* 组件树 */}
</MotionConfig>

注意事项

  • 不是所有动画都要禁用,只禁用可能引起不适的动画
  • 保留必要的状态反馈动画
  • 考虑提供静态替代方案
  • 测试在启用减少动画时的用户体验
  • 某些功能性动画(如进度条)可以保留

可访问性

遵循 WCAG 2.1 准则:

  • Success Criterion 2.3.3: 动画来自交互(AAA 级)
  • 尊重用户的系统偏好
  • 提供无动画的可用体验