Уважение prefers-reduced-motion

prefers-reduced-motion — это медиа-запрос CSS и соответствующая логика в веб-платформах, позволяющая учитывать предпочтения пользователя по минимизации анимаций и движения на экране. Он особенно важен для пользователей с повышенной чувствительностью к движению или проблемами с вестибулярным аппаратом. Игнорирование этих предпочтений может привести к дискомфорту и даже физическим недомоганиям. В контексте React Aria этот механизм используется для создания доступных интерфейсов, где анимации адаптируются под реальные предпочтения пользователя.


Определение и проверка

В вебе доступ к prefers-reduced-motion осуществляется через CSS или JavaScript. В CSS это выглядит так:

@media (prefers-reduced-motion: reduce) {
  .animated-element {
    animation: none;
    transition: none;
  }
}

В JavaScript проверка осуществляется с помощью window.matchMedia:

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

В React Aria рекомендуется использовать хук useReducedMotion, который абстрагирует проверку и обеспечивает реактивное поведение:

import { useReducedMotion } from '@react-aria/interactions';

function AnimatedButton() {
  const reducedMotion = useReducedMotion();

  return (
    <button style={{
      transition: reducedMotion ? 'none' : 'transform 0.3s ease-in-out'
    }}>
      Нажми меня
    </button>
  );
}

Ключевой момент: если reducedMotion равен true, анимации и переходы должны быть полностью отключены или заменены на минимальные эффекты.


Интеграция с анимациями

React Aria не предоставляет встроенных визуальных анимаций, но часто используется совместно с библиотеками вроде Framer Motion или React Spring. В таких случаях prefers-reduced-motion следует учитывать при создании анимационных компонентов.

Пример с Framer Motion:

import { motion } from 'framer-motion';
import { useReducedMotion } from '@react-aria/interactions';

function Box() {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      animate={shouldReduceMotion ? {} : { x: 100, opacity: 1 }}
      transition={{ duration: shouldReduceMotion ? 0 : 0.5 }}
    >
      Контент
    </motion.div>
  );
}

Если пользователь выбрал уменьшение анимации, свойства animate остаются статичными, а длительность анимации устанавливается в 0. Это предотвращает непреднамеренные эффекты движения и делает интерфейс безопасным.


Практическая реализация в компонентах React Aria

1. Кнопки и интерактивные элементы

Для кнопок с hover-эффектами:

import { useButton } from '@react-aria/button';
import { useReducedMotion } from '@react-aria/interactions';

function AccessibleButton(props) {
  const ref = React.useRef();
  const { buttonProps } = useButton(props, ref);
  const reducedMotion = useReducedMotion();

  return (
    <button
      {...buttonProps}
      ref={ref}
      style={{
        transition: reducedMotion ? 'none' : 'transform 0.2s',
      }}
    >
      {props.children}
    </button>
  );
}

2. Модальные окна и выпадающие списки

Для появления и скрытия модальных окон или Popover:

import { useOverlay } from '@react-aria/overlays';
import { useReducedMotion } from '@react-aria/interactions';

function Modal({ isOpen, onClose, children }) {
  const ref = React.useRef();
  const { overlayProps } = useOverlay({ isOpen, onClose }, ref);
  const reducedMotion = useReducedMotion();

  return isOpen ? (
    <div
      {...overlayProps}
      ref={ref}
      style={{
        opacity: reducedMotion ? 1 : 0,
        transform: reducedMotion ? 'none' : 'translateY(-10px)',
        transition: reducedMotion ? 'none' : 'all 0.3s ease-in-out'
      }}
    >
      {children}
    </div>
  ) : null;
}

3. Карточки и списки с анимацией появления

Для списков с анимацией появления элементов:

import { useReducedMotion } from '@react-aria/interactions';
import { motion, AnimatePresence } from 'framer-motion';

function AnimatedList({ items }) {
  const reducedMotion = useReducedMotion();

  return (
    <ul>
      <AnimatePresence>
        {items.map(item => (
          <motion.li
            key={item.id}
            initial={reducedMotion ? {} : { opacity: 0, y: -10 }}
            animate={reducedMotion ? {} : { opacity: 1, y: 0 }}
            exit={reducedMotion ? {} : { opacity: 0, y: 10 }}
            transition={{ duration: reducedMotion ? 0 : 0.3 }}
          >
            {item.content}
          </motion.li>
        ))}
      </AnimatePresence>
    </ul>
  );
}

Рекомендации по использованию

  • Всегда применять useReducedMotion к анимационным компонентам, чтобы избежать потенциального дискомфорта.
  • При prefers-reduced-motion лучше минимизировать не только анимации, но и параллакс, мерцающие эффекты и сложные переходы.
  • Если анимация несёт важную функциональную информацию (например, индикатор загрузки), она должна оставаться, но быть максимально ненавязчивой.

Вывод: соблюдение prefers-reduced-motion является обязательным элементом доступного интерфейса. В React Aria это достигается через хуки и адаптацию стилей, анимаций и переходов под предпочтения пользователя. Такой подход делает интерфейс безопасным, комфортным и инклюзивным.