prefers-reduced-motion читает системную настройку "меньше анимаций", которую пользователь включает в операционной системе, а не в браузере - и позволяет CSS среагировать на неё. Свойство называется "reduced", а не "no" motion: задача не убрать анимации совсем, а смягчить резкое движение (масштабирование, вращение, панорамирование) для тех, кому оно физически неприятно, оставив при этом функциональную обратную связь на месте.
В этом уроке
Попробуй прямо сейчас: спиннер крутится с обычной скоростью (1s на оборот). В настройках своей операционной системы включи "уменьшение движения" (на macOS - Универсальный доступ → Дисплей → Уменьшение движения, на Windows 11 - Спец. возможности → Визуальные эффекты) и обнови блокнот - анимация замедлится до 3s на оборот вместо того, чтобы исчезнуть полностью: спиннер остаётся функциональным сигналом, просто менее резким.
Что такое prefers-reduced-motion и как его ловить в CSS
Два значения: no-preference (по умолчанию, если пользователь ничего не настраивал) и reduce (пользователь явно включил "меньше анимаций" в настройках своей операционной системы - Windows, macOS, iOS, Android, всё это делается на уровне ОС, а не браузера). CSS только читает уже готовую системную настройку через media-запрос, сам её не создаёт и не хранит.
@media (prefers-reduced-motion: reduce) {
.fade-in { animation: none; }
} - полная запись со значением.
@media (prefers-reduced-motion) {
.fade-in { animation: none; }
} - сокращённая запись без : reduce, равнозначна полной.
Собственная проверка (Playwright, page.emulate_media - реальная эмуляция системной настройки браузером, не догадка на глаз): при эмуляции prefers-reduced-motion: reduce вычисленный (computed) стиль элемента действительно менялся согласно правилу внутри media-запроса. Сокращённая запись без : reduce тоже проверена отдельно на тестовом блоке - он менял цвет ТОЛЬКО при активной эмуляции reduce, оставаясь исходным при no-preference по умолчанию - подтверждает, что короткая запись реально равнозначна полной, а не просто предположение из документации.
Как правильно смягчить анимации, а не убрать их совсем
Готовый приём для всей страницы разом - универсальный селектор внутри media-запроса: @media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
} - !important обязателен, чтобы гарантированно перебить обычные правила анимации независимо от их специфичности. Обрати внимание на 0.001ms, а не animation: none или duration: 0 - это прямое развитие уже разобранного в уроке про индикатор загрузки: если где-то на странице JavaScript слушает событие animationend или transitionend, полное отключение анимации это событие вообще не породит и сломает логику скрипта, а крошечная ненулевая длительность порождает событие всё равно, оставаясь практически незаметной глазом.
Как уже упоминалось в уроке про анимацию появления при загрузке, свойство называется "reduced", а не "no" motion - и это не формальность. Резкое движение вроде масштабирования, вращения на весь экран или панорамирования способно физически вызвать дискомфорт у части пользователей (речь о вестибулярной чувствительности к движению) - именно от него стоит избавляться в первую очередь. А функциональные анимации, которые сообщают о реальном состоянии страницы (тот же спиннер загрузки), часто сознательно оставляют - только заменяя резкое движение на что-то спокойнее, например изменение прозрачности или цвета, а не убирая обратную связь совсем.
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; }
} - "ядерный" вариант убирает вообще всё, включая функциональные подсказки состояния и события, на которые может быть завязан JS.
@media (prefers-reduced-motion: reduce) {
.decorative-slide { animation: none; }
.spinner { animation-duration: 3s; }
} - декоративное движение отключается полностью, а функциональный спиннер остаётся, просто медленнее и спокойнее.
Частые ошибки новичков
| Ошибка | Что происходит | Как исправить |
|---|---|---|
| Считать, что prefers-reduced-motion: reduce значит "отключить анимации полностью" | Пропадает и функциональная обратная связь (спиннеры, индикаторы состояния), которая пользователю нужна | Смягчать резкое движение, а не убирать вообще всё - оставлять функциональные анимации, но спокойнее |
| Забыть !important в глобальном правиле для всей страницы | Обычные правила анимации с равной или большей специфичностью продолжают побеждать в каскаде | Явно указывать !important в приёме-"выключателе" внутри reduced-motion блока |
| Ставить animation: none или duration: 0 вместо крошечной ненулевой длительности | Событие animationend/transitionend вообще не порождается - ломает JS-логику, которая на него завязана | Использовать animation-duration: 0.001ms вместо полного отключения |
| Размещать reduced-motion правило раньше обычного правила с той же специфичностью в исходном коде CSS | При равной специфичности побеждает то правило, что идёт позже по порядку - обычное правило перебивает reduced-motion | Писать reduced-motion блок ПОСЛЕ обычных правил анимации в файле стилей |
Шпаргалка
| Запись | Что означает |
|---|---|
| @media (prefers-reduced-motion: reduce) | Пользователь включил "меньше анимаций" в настройках ОС |
| @media (prefers-reduced-motion) | Сокращённая запись, равнозначна : reduce |
| no-preference | Значение по умолчанию - пользователь ничего не настраивал |
| animation-duration: 0.001ms !important | Глобальный "выключатель" - почти незаметно, но события animationend всё ещё срабатывают |
| Reduced, не Zero | Смягчить резкое движение, а не убрать всю обратную связь |
Коротко: отвечаем на вопросы
Что означает prefers-reduced-motion: reduce и как пользователь его включает?
Это значит, что пользователь явно включил настройку "меньше анимаций" в своей операционной системе - на Windows, macOS, iOS или Android, всегда на уровне ОС, а не браузера. CSS-медиа-запрос prefers-reduced-motion только читает уже готовую системную настройку через @media (prefers-reduced-motion: reduce), сам её не создаёт. Значение по умолчанию, если пользователь ничего не настраивал, - no-preference.
Отключает ли prefers-reduced-motion анимации полностью?
Нет, и это важный нюанс: свойство называется "reduced", а не "no" motion. Задача - смягчить резкое движение вроде масштабирования, вращения или панорамирования, которое способно физически вызвать дискомфорт у части пользователей, а не убрать вообще все анимации. Функциональные анимации, сообщающие о реальном состоянии страницы (например спиннер загрузки), часто сознательно оставляют - просто заменяют резкое движение на что-то спокойнее, вроде изменения прозрачности или цвета.
Как одним блоком CSS смягчить все анимации на странице для таких пользователей?
Готовый приём - универсальный селектор внутри @media (prefers-reduced-motion: reduce): animation-duration и transition-duration на 0.001ms с !important, animation-iteration-count: 1 с !important. !important нужен, чтобы гарантированно перебить обычные правила анимации. Важно использовать именно крошечную ненулевую длительность, а не animation: none или duration: 0 - иначе событие animationend или transitionend, на которое может быть завязан JavaScript, вообще не сработает.
Работает ли @media (prefers-reduced-motion) без указания : reduce?
Да, это сокращённая запись, равнозначная полной @media (prefers-reduced-motion: reduce). Собственная проверка через эмуляцию системной настройки в браузере подтвердила: правило внутри сокращённой записи срабатывает ровно тогда же, когда сработала бы полная запись со значением reduce, и не срабатывает при значении по умолчанию no-preference.