Motion / UX / практический материал
Как правильно использовать анимации на сайте
Как добавить анимацию появления блоков, кнопок и интерфейса без тормозов. CSS, Intersection Observer, transform, opacity и reduced motion.
- Дмитрий Лаврушин
- 23 августа 2026
- 5 минут
Анимация объясняет изменение состояния или направляет внимание.
Лаборатория движения
Выберите эффект, перезапустите демонстрацию и скопируйте минимальный CSS.Четыре безопасных паттерна анимации
Мягкое появление подходит для смысловых блоков без смены геометрии.
.motion-item {
opacity: 0;
transition: opacity 420ms ease;
}
.motion-item.is-visible { opacity: 1; }
Хорошая анимация объясняет действие. Она показывает, что кнопка нажалась, меню открылось, блок появился, а форма успешно отправлена. Плохая анимация существует сама по себе и мешает прочитать страницу.
Для простых состояний достаточно CSS. JavaScript нужен, когда эффект зависит от прокрутки, данных или последовательности действий пользователя.
Безопасная анимация появления блока
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity .5s ease, transform .5s ease;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.reveal {
opacity: 1;
transform: none;
transition: none;
}
}const items = document.querySelectorAll('.reveal');
const observer = new IntersectionObserver((entries, instance) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add('is-visible');
instance.unobserve(entry.target);
});
}, { threshold: 0.15 });
items.forEach((item) => observer.observe(item));IntersectionObserver сообщает, когда элемент входит в область просмотра. Браузеру не приходится постоянно вычислять положение каждого блока при событии scroll. После первого появления наблюдение отключается.
Матрица вариантов: какой эффект выбрать
| Задача | Эффект | Длительность | Техника |
|---|---|---|---|
| Появление смыслового блока | Fade + небольшой translateY | 400–600 мс | CSS + Intersection Observer |
| Реакция кнопки | Scale до 0.97 и возврат | 120–180 мс | CSS :active |
| Смена вкладки | Crossfade | 180–260 мс | CSS-класс + JavaScript |
| Раскрытие ответа | Grid rows | 250–350 мс | <details> + CSS |
| Серия карточек | Stagger | 60–100 мс между элементами | CSS-переменная --delay |
Начните с одного эффекта на один тип действия. Если карточки появляются снизу, не заставляйте соседний блок одновременно вылетать сбоку и вращаться. Повторяемая система воспринимается дороже набора случайных эффектов.
Шаблон токенов движения
Соберите длительности и кривые в переменные. Тогда весь интерфейс можно ускорить или сделать спокойнее в одном месте.
:root {
--motion-fast: 160ms;
--motion-base: 360ms;
--motion-slow: 620ms;
--ease-ui: cubic-bezier(.2, .8, .2, 1);
--ease-out: cubic-bezier(.16, 1, .3, 1);
}
.button {
transition:
transform var(--motion-fast) var(--ease-ui),
box-shadow var(--motion-fast) var(--ease-ui);
}
.button:hover { transform: translateY(-2px); }
.button:active { transform: translateY(0) scale(.97); }Шаблон stagger для группы карточек
Каждая карточка получает собственную задержку через CSS-переменную. JavaScript только добавляет класс контейнеру.
<div class="feature-grid" data-stagger>
<article style="--delay: 0ms">Структура</article>
<article style="--delay: 80ms">Дизайн</article>
<article style="--delay: 160ms">Разработка</article>
</div>.feature-grid > * {
opacity: 0;
transform: translateY(24px);
transition:
opacity 520ms var(--ease-out) var(--delay),
transform 520ms var(--ease-out) var(--delay);
}
.feature-grid.is-visible > * {
opacity: 1;
transform: none;
}const grid = document.querySelector('[data-stagger]');
new IntersectionObserver(([entry], observer) => {
if (!entry.isIntersecting) return;
grid.classList.add('is-visible');
observer.disconnect();
}, { threshold: 0 }).observe(grid);Используйте threshold: 0 для длинных блоков. Если поставить 0.15, элемент высотой в несколько экранов может никогда не набрать нужную долю пересечения и останется невидимым при открытии страницы по якорю.
Шаблон раскрывающегося блока без библиотеки
Нативный <details> сохраняет работу с клавиатурой. CSS отвечает только за визуальное состояние.
<details class="faq-motion">
<summary>Сколько анимаций нужно странице?</summary>
<div><p>Столько, сколько объясняют состояние или следующий шаг.</p></div>
</details>.faq-motion > div {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows var(--motion-base) var(--ease-ui);
}
.faq-motion > div > * { overflow: hidden; }
.faq-motion[open] > div { grid-template-rows: 1fr; }Шаблон переключения вкладок с crossfade
const tabs = document.querySelectorAll('[data-tab]');
const panels = document.querySelectorAll('[data-panel]');
tabs.forEach((tab) => {
tab.addEventListener('click', () => {
tabs.forEach((item) => {
const active = item === tab;
item.setAttribute('aria-selected', String(active));
});
panels.forEach((panel) => {
panel.hidden = panel.dataset.panel !== tab.dataset.tab;
});
});
});[data-panel] {
animation: panel-in var(--motion-base) var(--ease-out) both;
}
@keyframes panel-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}Какие свойства лучше анимировать
Старайтесь использовать transform и opacity. Они обычно не меняют геометрию страницы. Анимация width, height, top или left может запускать перерасчет макета и перерисовку.
Для кнопки достаточно от 160 до 250 мс. Для появления крупного блока обычно хватает от 400 до 700 мс. Это не жесткий стандарт. Скорость нужно проверять на реальном интерфейсе и мобильном устройстве.
Не задавайте transition: all. Укажите свойства явно. will-change также не нужно добавлять ко всем элементам. Он резервирует ресурсы и полезен только для действительно тяжелых эффектов.
Где анимация приносит пользу
Используйте движение в пяти сценариях: обратная связь после клика, открытие меню, переключение вкладок, появление важного контента и демонстрация процесса. В продающем блоке анимация может последовательно показать проблему, решение и результат. На карточке товара она помогает увидеть состояние наведения или выбранный вариант.
Не скрывайте основной текст до запуска JavaScript. Если скрипт не загрузится, пользователь и робот все равно должны увидеть содержание. Для декоративных эффектов учитывайте prefers-reduced-motion. Пользователь может отключить движение в настройках системы.
Безопасная схема работает наоборот: контент виден по умолчанию, а подготовительное скрытое состояние включается классом html.motion-ready только после запуска JavaScript.
.reveal { opacity: 1; transform: none; }
.motion-ready .reveal {
opacity: 0;
transform: translateY(20px);
}
.motion-ready .reveal.is-visible {
opacity: 1;
transform: none;
}document.documentElement.classList.add('motion-ready');
window.addEventListener('load', () => {
document.querySelectorAll('.reveal').forEach((item) => {
const rect = item.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
item.classList.add('is-visible');
}
});
});Разделяйте микровзаимодействия и декоративное появление. Состояние кнопки после клика важно для управления. Плавный выезд каждой карточки является оформлением. Если нужно сокращать код или ускорять страницу, декоративные эффекты удаляются первыми.
Как проверить результат
Откройте страницу на обычном смартфоне. Проверьте прокрутку, работу меню и скорость реакции кнопок. В DevTools запишите Performance-профиль. Если эффект вызывает постоянный Layout или Paint, упростите его. Затем проверьте CLS и INP в PageSpeed Insights.
Анимация должна поддерживать интерфейс, а не демонстрировать возможности разработчика. Если после ее удаления действие становится менее понятным, эффект полезен. Если ничего не меняется, его можно убрать.