Анимация элементов при скролле — классическая задача, которую часто решают либо тяжелыми библиотеками, либо громоздким кодом. В этой статье мы создадим систему, которая поддерживает Tailwind CSS и корректно работает даже если у пользователя отключен JavaScript.
JavaScript: Intersection Observer
Для отслеживания состояния элементов будем использовать IntersectionObserver. Он не нагружает процессор при скролле и является более подходящим аналогом, чем eventListener.
При загрузке скрипта устанавливаем класс для документа. В дальнейшем будем применять анимацию только, если JavaScript включен. Это позволит поисковым роботам и пользователям, у которых по каким-то причинам выключен JavaScript, видеть неискажённую версию сайта.
document.documentElement.classList.add('js-enabled')
const observerOptions = {
root: null,
rootMargin: '0px 0px -10% 0px',
threshold: 0.2,
}
const animationObserver = new IntersectionObserver(onEntry, observerOptions)
const animationElements = document.querySelectorAll('[data-animation]')
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.documentElement.classList.add('anim-ready')
animationElements.forEach(el => animationObserver.observe(el))
})
})
function onEntry(entries) {
entries.forEach(entry => {
const { target, isIntersecting } = entry
const shouldRepeat = target.dataset.animationRepeat !== 'false'
if (isIntersecting) {
target.dataset.animation = 'active'
if (!shouldRepeat) animationObserver.unobserve(target)
} else if (shouldRepeat) target.dataset.animation = ''
})
}
Вложенные requestAnimationFrame нужны для того, чтобы разделить рендер на 3 этапа:
- без анимации
- с классом
.js-enabled - с классом
.js-enabled.anim-ready
Скрипт подразумевает, что мы будем устанавливать атрибут [data-animation] для элементов, которые нужно анимировать.
<div data-animation></div>
И [data-animation-repeat="false"] для тех, у которых анимация будет срабатывать только однократно.
<div data-animation data-animation-repeat="false"></div>
По умолчанию подразумеваем, что все анимации должны повторяться, если не установлен
[data-animation-repeat="false"].
Применение
В упрощённой версии уже можем пользоваться:
.test-elem {
transition-duration: 0.5s;
}
.test-elem:not([data-animation='active']) {
opacity: 0;
transform: translateY(5rem);
}
Или в Tailwind создадим что-то такое через :not
<div
class='duration-300 [&:not([data-animation="active"])]:translate-y-10 [&:not([data-animation="active"])]:opacity-0'
data-animation
></div>
Минус в этом тот, что при выключенном JavaScript элемент в своё нормальное положение не встанет.
Улучшаем компонент
Добавим в наш CSS следующий код:
[data-animation] {
opacity: 1;
translate: 0;
scale: 1;
}
.js-enabled [data-animation] {
--_op: var(--anim-opacity, 1);
--_x: var(--anim-translate-x, 0);
--_y: var(--anim-translate-y, 0);
--_s: var(--anim-scale, 1);
opacity: var(--_op);
translate: var(--_x) var(--_y);
scale: var(--_s);
transition: none;
}
.js-enabled.anim-ready [data-animation] {
transition-property: opacity, translate, scale;
transition-duration: var(--anim-duration, 0.3s);
transition-timing-function: ease-out;
will-change: opacity, translate, scale;
}
.js-enabled.anim-ready [data-animation='active'] {
--_op: 1;
--_x: 0;
--_y: 0;
--_s: 1;
}
Здесь мы описали часто используемые параметры для анимации, такие как opacity, translateX, translateY, scale и длительность анимации.
Итого, наши заранее подготовленные переменные:
--anim-opacity– начальное значение прозрачности--anim-translate-x– смещение по оси X--anim-translate-y– смещение по оси Y--anim-scale– увеличение/уменьшение размера--anim-duration– длительность анимации
Теперь можно использовать такую запись, даже ничего не прописывая в CSS для нашего компонента. В примере: элемент появится снизу из прозрачного состояния за 0.5 секунд.
<div
data-animation
style="--anim-duration: 0.5s; --anim-opacity: 0; --anim-translate-y: 5rem"
></div>
Если нам понадобится сделать задержку, то просто добавим к инлайн-стилям style="transition-delay: 1s;".
Пример:
Пример анимации, параграф появляется снизу
Заботимся о тех, кто ненавидит анимации
Добавим в CSS такой сброс. Если пользователь указал в настройках браузера prefers-reduced-motion: reduce, или если устройство не поддерживает частую смену кадров, то устанавливаем все анимации на сайте мгновенными. Эту настройку стоит применять на любый сайтах.
@media screen and (prefers-reduced-motion: reduce), (update: slow) {
* {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
transition-delay: 0.001ms !important;
}
}
Итоги
Итого, имеем следующие плюсы:
- Не используем сторонние библиотеки
- Используем более быстрый
IntersectionObserverвместоeventListener - Не пишем классы для изначального состояния для каждого компонента
- Не показываем анимацию тем, кто её выключил в настройках
- Если скрипт не заработал, то сайт работает нормально
Для полного счастья не хватает только поддержки добавляемых в DOM новых элементов и staggered-настроек для того, чтобы не перечислять вручную delay у вложенных элементов.