Skip to content
PПромтбук
RUEN
02Motion

Стратегия prefers-reduced-motion

Что отключать полностью, что заменять на fade, как тестировать и какие элементы НИКОГДА не должны быть motion-зависимыми.

Спроектируй стратегию prefers-reduced-motion для проекта на стеке: {{stack}}. Цель — соответствие WCAG 2.3.3, нулевой вестибулярный риск, сохранение смысла интерфейса.

1. Категоризация анимаций

Раздели всю анимацию проекта на три бакета:

БакетЧто входитДействие при reduced-motion
A. Декоративнаяparallax, auto-play видео, плавающие частицы, hero-петли, marqueeПолностью отключить
B. Сюжетнаяslide-in карточек, page transitions, modal scale, accordion expandЗаменить на fade 100-150ms или мгновенный показ
C. Сигнальнаяfocus ring, loading spinner, прогресс-бар, toast appearance, status changeСохранить, но упростить (короткий fade, без движения по оси)

Правило: если анимация передаёт смысл (что-то загружается, статус изменился) — её нельзя выключать целиком. Иначе пользователь потеряет информацию.

2. Технический паттерн

/* База: motion-friendly defaults */
.card { transition: transform 250ms var(--ease-out), opacity 250ms; }

@media (prefers-reduced-motion: reduce) {
  /* A. Декоративная — полный off */
  .parallax, .marquee, .particle-bg { animation: none !important; transform: none !important; }

  /* B. Сюжетная — fade вместо slide */
  .card { transition: opacity 150ms ease; transform: none !important; }
  .modal-enter { transform: none; }

  /* C. Сигнальная — короче, без движения */
  .toast { transition: opacity 120ms ease; transform: none; }
  .spinner { animation-duration: 1.2s; } /* НЕ убираем — это статус */
}

Глобальный kill-switch (* { animation-duration: 0.01ms !important }) — запрещён: он ломает сигнальные индикаторы.

3. JS-стек (Framer Motion / GSAP)

import { useReducedMotion } from "framer-motion";

const shouldReduce = useReducedMotion();
const variants = shouldReduce
  ? { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { duration: 0.15 } } }
  : { hidden: { opacity: 0, y: 16 }, visible: { opacity: 1, y: 0, transition: { duration: 0.3 } } };

GSAP: проверяй window.matchMedia('(prefers-reduced-motion: reduce)').matches и заменяй .from(el, {y: 40}) на .set(el, {autoAlpha: 1}).

4. Что НЕ должно быть motion-зависимым (никогда)

  • Loading indicators — статус процесса. Замени spin на pulsing dot или текст «Загрузка…».
  • Focus indicators — обязаны быть видны сразу, без transition opacity.
  • Status changes (success/error) — цвет + иконка + ARIA-live; анимация — бонус.
  • Hover affordance — кнопка должна выглядеть кликабельной без анимации (контраст, форма).
  • Form validation — ошибка показывается мгновенно, не через slide-in.
  • Navigation state (active tab, current page) — статичные стили обязаны работать без анимации.

5. Чек-лист тестирования

  1. macOS: System Settings → Accessibility → Display → Reduce motion.
  2. iOS: Settings → Accessibility → Motion → Reduce Motion.
  3. Windows 11: Settings → Accessibility → Visual effects → Animation effects → Off.
  4. Chrome DevTools: Rendering → Emulate CSS media feature prefers-reduced-motion: reduce.
  5. Пройди ключевые сценарии: понятен ли статус? Видны ли переходы между экранами? Не «телепортируется» ли модалка?
  6. Lighthouse Accessibility audit + axe DevTools — проверь, что нет нарушений 2.3.3 Three Flashes.

6. Формат вывода

## Аудит motion-проекта
| Компонент | Бакет (A/B/C) | Что делает сейчас | Что делает при reduced |
|---|---|---|---|
| HeroParallax | A | translateY на scroll | display none на mobile, off |
| CardGrid | B | stagger slide-up | fade 150ms одновременно |
| Spinner | C | rotate infinite | pulse opacity 0.4↔1, 1.2s |

## Изменения в коде
[файлы + диффы]

## Чек-лист QA
- [ ] Все 6 пунктов из секции 5 пройдены

Анти-паттерны

  • ❌ Глобальный * { transition: none !important } — ломает focus ring, ARIA-визуализацию, spinner.
  • ❌ Полное отключение loading-анимаций — пользователь думает, что зависло.
  • ❌ Reduced-motion = «выключить всё красивое» — это про вестибулярный риск (движение по оси), не про эстетику.
  • ❌ Тестирование только в Chrome DevTools без реального OS-флага — поведение медиа-запроса в эмуляции иногда расходится с системным.
  • ❌ Замена slide на ещё более резкий «snap» — тоже триггерит вестибулярку. Используй fade.
  • ❌ Игнор JS-анимаций (GSAP, Framer Motion, Lottie) при наличии CSS-fallback — JS перезатирает CSS.
  • ❌ Lottie-анимация без prefers-reduced-motion fallback — самые тяжёлые для людей с вестибулярными нарушениями.
К подразделу «Motion»
Похожие промты