prefers-reduced-motion: reduce становится ключевым
входом в архитектуру интерфейсной анимации, когда Popmotion используется
в продуктивных UI-сценариях. Игнорирование этого медиазапроса приводит к
избыточной визуальной нагрузке, ухудшению читаемости интерфейса и прямым
барьерам для пользователей с вестибулярными расстройствами, мигренями и
когнитивной чувствительностью к движению.
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}
Подобный глобальный сброс часто оказывается слишком грубым инструментом. Popmotion работает на уровне JavaScript-движка анимаций, поэтому управление доступностью должно происходить не только на CSS-уровне, но и в логике вызова анимаций.
Базовый слой доступности формируется через
window.matchMedia, который позволяет определить
предпочтение к сниженной анимации до запуска любой Popmotion-логики.
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
На этом уровне формируется архитектурное правило: любая анимация в системе должна зависеть от единого флага. Это исключает ситуацию, когда отдельные компоненты продолжают анимироваться независимо от глобальных настроек.
Popmotion предоставляет низкоуровневые примитивы
(animate, spring, timeline), что
требует обёртки для управления доступностью.
import { animate } from 'popmotion';
function safeAnimate(options) {
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (prefersReducedMotion) {
if (options.onUpdate) {
options.onUpdate(options.to);
}
if (options.onComplete) {
options.onComplete();
}
return { stop: () => {} };
}
return animate(options);
}
Такой слой превращает анимации в условные эффекты: визуальная динамика существует только при отсутствии запроса на снижение движения.
Ключевой архитектурный сдвиг заключается в отказе от «движения как единственного носителя информации». Анимация не должна быть единственным способом выразить изменение состояния.
При раскрытии панели:
const heightAnimation = {
from: 0,
to: 300,
onUpdate: v => {
panel.style.height = `${v}px`;
}
};
if (prefersReducedMotion) {
panel.style.height = '300px';
} else {
animate(heightAnimation);
}
Состояние становится первичным, а анимация — вторичной надстройкой.
Spring-модель Popmotion (spring) часто воспринимается
как более естественная, но именно она может создавать избыточное
микродвижение, особенно при частых изменениях состояния интерфейса.
import { spring } from 'popmotion';
spring({
from: 0,
to: 1,
stiffness: 200,
damping: 20,
onUpdate: v => {
element.style.transform = `scale(${v})`;
}
});
При включённой настройке снижения движения spring-модель заменяется на дискретное переключение состояний:
if (prefersReducedMotion) {
element.style.transform = 'scale(1)';
} else {
spring({
from: 0,
to: 1,
onUpdate: v => {
element.style.transform = `scale(${v})`;
}
});
}
Важно учитывать, что даже «мягкие» физические модели создают визуальный шум при массовом применении (списки, карточки, таблицы).
timeline в Popmotion используется для сложных цепочек
анимаций. Именно здесь чаще всего возникает проблема перегрузки
движения.
import { timeline } from 'popmotion';
timeline([
[element1, { opacity: [0, 1] }, 300],
[element2, { x: [100, 0] }, 500],
[element3, { scale: [0.8, 1] }, 300]
]);
При сниженной анимации последовательность должна схлопываться в мгновенное применение конечных значений:
if (prefersReducedMotion) {
element1.style.opacity = 1;
element2.style.transform = 'translateX(0px)';
element3.style.transform = 'scale(1)';
} else {
timeline([
[element1, { opacity: [0, 1] }, 300],
[element2, { x: [100, 0] }, 500],
[element3, { scale: [0.8, 1] }, 300]
]);
}
Анимации, особенно основанные на transform, создают
эффект движения, который может нарушать восприятие статичной структуры
интерфейса. В доступных интерфейсах важно избегать:
Popmotion позволяет легко создавать такие эффекты, но архитектура доступности требует их контроля.
Даже при отсутствии полного отключения движения важна деградация сложности. Вместо полной анимации применяется сокращённая версия:
const duration = prefersReducedMotion ? 0 : 400;
animate({
from: 0,
to: 100,
duration,
onUpdate: v => {
element.style.left = `${v}px`;
}
});
Анимации не должны мешать фокусу. Popmotion-эффекты часто применяются к модальным окнам, меню и всплывающим панелям, где критична клавиатурная навигация.
При открытии модального окна:
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
button.setAttribute('disabled', 'true');
modal.querySelector('button').focus();
if (!prefersReducedMotion) {
animate({
from: 0,
to: 1,
onUpdate: v => {
modal.style.opacity = v;
}
});
}
Семантика доступности всегда должна выполняться до визуальной анимации.
Анимация не должна заменять ARIA-атрибуты. Popmotion может визуально отражать состояние, но доступность должна опираться на разметку:
aria-expandedaria-hiddenaria-livebutton.setAttribute('aria-expanded', 'true');
panel.setAttribute('aria-hidden', 'false');
Анимация становится визуальным отражением уже установленного состояния, а не его причиной.
Popmotion позволяет создавать сложные сцены с несколькими слоями движения. Однако параллакс и глубинные смещения часто вызывают дезориентацию.
В доступной версии интерфейса такие эффекты заменяются статичной композицией слоёв.
if (prefersReducedMotion) {
background.style.transform = 'none';
foreground.style.transform = 'none';
} else {
animate({
from: 0,
to: 1,
onUpdate: v => {
background.style.transform = `translateY(${v * 20}px)`;
foreground.style.transform = `translateY(${v * 10}px)`;
}
});
}
В системах, построенных на Popmotion, устойчивой становится модель, в которой:
Такой подход предотвращает рассыпание условий по компонентам и упрощает тестирование интерфейса в режимах без движения.
При отсутствии анимации интерфейс должен оставаться полностью функциональным и визуально консистентным. Любая анимация в Popmotion обязана иметь конечное состояние, идентичное завершению анимации.
Это означает:
Анимация перестаёт быть процессом и становится способом отображения перехода между состояниями.