Плавная анимация перемещения с помощью CSS

Для плавного перемещения задавайте смещение через transform и анимируйте его изменение. Браузер сможет двигать элемент без пересчета положения соседей. Это сокращает работу при отрисовке, но не гарантирует отсутствие рывков.

Используйте transition для смены состояния, @keyframes для повторяемой последовательности, а JavaScript – для расчета координат. В конце статьи вы получите рабочий фрагмент и проверите его в DevTools.

Почему top и left могут вызывать рывки

Браузер рассчитывает размеры и положение элементов на этапе layout. Затем он рисует содержимое и собирает изображение из слоев. Последний этап называют композитингом.

Изменение width, margin или left может запускать пересчет геометрии. Во время анимации браузер повторяет эту работу для промежуточных значений. На сложной странице вычисления могут задерживать вывод кадров.

СвойствоЧто пересчитывает браузер
top, leftПоложение позиционированного элемента; затем может потребоваться перерисовка
margin, padding, width, heightГеометрию элемента и, при зависимости, положение соседей
transformВизуальное преобразование без изменения обычного потока; при подходящем слое достаточно композитинга
opacityПрозрачность без пересчета геометрии; при подходящем слое достаточно композитинга

Свойства top и left влияют на положение только при соответствующем позиционировании. Значения absolute и fixed могут ограничить область пересчета, но не отменяют вычисления геометрии самого элемента.

Используйте transform, когда нужно визуально сдвинуть готовый блок. Преобразование сохраняет исходное место элемента в потоке страницы, поэтому соседи не раздвигаются. Движущийся блок может перекрыть их.

Если задача требует менять расположение соседей, одного преобразования недостаточно. Для такой задачи меняйте свойства макета осознанно и не запускайте их изменение на каждом кадре.

Перемещение через translate3d и translate

Функция translate3d(x, y, z) задает смещение по трем осям. Аргумент X отвечает за горизонталь, Y – за вертикаль, Z – за глубину. Для движения в плоскости страницы оставьте Z равным 0.

Смещение элемента по горизонтальной и вертикальной осям с помощью translate3d

Положительный X сдвигает элемент вправо, положительный Y – вниз. Чтобы переместить блок на 20 пикселей вправо и вверх, задайте отрицательный Y:

.foo {
    transform: translate3d(20px, -20px, 0);
}

Для плоского перемещения подходит и translate(20px, -20px). Функции translateX(20px) и translateY(-20px) меняют только одну координату.

Отдельное CSS-свойство translate позволяет задать смещение независимо от transform:

.foo {
    translate: 20px -20px;
}

Свойство translate поддерживается современными браузерами. Для него указывайте в переходе transition: translate 0.2s ease-in-out. В примерах с transform переход должен отслеживать именно transform.

Нулевой Z в translate3d() не создает видимой глубины. Отличие от translate() само по себе не гарантирует более плавного движения: браузер решает, нужен ли отдельный слой.

Синтаксис преобразований описывает документация MDN по transform. Для этих примеров в современных браузерах префиксы не нужны.

translate3d в transition и @keyframes

Переход подходит для смены состояния, например при наведении курсора. Ключевые кадры задают последовательность движения и позволяют повторять ее независимо от наведения.

Переход по наведению

Укажите transition в основном правиле элемента, затем задайте конечное смещение. Браузер плавно проиграет движение вперед и возврат:

.pictureContainer img {
    display: block;
    transform: translate3d(0, 0, 0);
    transition: transform .2s ease-in-out;
}

.pictureContainer:hover img {
    transform: translate3d(0, -150px, 0);
}

В этом примере изображение поднимается на 150 пикселей за 0,2 секунды. Наведение отслеживает контейнер, который остается на месте. Поэтому изображение не уходит из-под указателя во время движения.

Для интерактивных элементов добавляйте состояние :focus-visible или :focus-within. Пользователь клавиатуры тоже должен получать визуальный отклик.

Движение через ключевые кадры

Создайте правило @keyframes и подключите его через свойство animation:

.dot {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: royalblue;
    animation: bobble 1s infinite;
}

@keyframes bobble {
    0% {
        transform: translate3d(50px, 40px, 0);
        animation-timing-function: ease-in;
    }
    50% {
        transform: translate3d(50px, 50px, 0);
        animation-timing-function: ease-out;
    }
    100% {
        transform: translate3d(50px, 40px, 0);
    }
}

@media (prefers-reduced-motion: reduce) {
    .dot {
        animation: none;
        transform: translate3d(50px, 40px, 0);
    }
}

Круг движется между координатами Y 40 и 50 пикселей. Цикл занимает 1 секунду и повторяется бесконечно. При системном запросе уменьшить движение круг остается неподвижным.

Параметры повторения и направления движения описывает документация MDN по @keyframes.

Перемещение из JavaScript

JavaScript может менять item.style.transform напрямую. Значение свойства передавайте строкой: `translate3d(${x}px, ${y}px, 0)`.

Функция CSS translate3d() не существует как встроенная функция JavaScript. Для покадрового управления используйте requestAnimationFrame().

Рассчитывайте смещение по времени, чтобы скорость не зависела от количества кадров. Следующий пример перемещает квадрат на 200 пикселей вправо и возвращает его по повторному нажатию.

<button type="button" id="move">Переместить</button>
<div class="track">
    <div class="item" id="item"></div>
</div>

<style>
.track { width: 260px; padding: 12px; background: #eee; }
.item { width: 40px; height: 40px; background: royalblue; }
</style>

<script>
const item = document.querySelector("#item");
const button = document.querySelector("#move");
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
let x = 0;
let moved = false;

button.addEventListener("click", () => {
    const from = x;
    const to = moved ? 0 : 200;
    moved = !moved;

    if (reducedMotion.matches) {
        x = to;
        item.style.transform = `translate3d(${x}px, 0, 0)`;
        return;
    }

    button.disabled = true;
    let start;

    function frame(time) {
        if (start === undefined) start = time;
        const progress = Math.min((time - start) / 500, 1);
        x = from + (to - from) * progress;
        item.style.transform = `translate3d(${x}px, 0, 0)`;

        if (progress < 1) {
            requestAnimationFrame(frame);
        } else {
            button.disabled = false;
        }
    }

    requestAnimationFrame(frame);
});
</script>

Нажмите кнопку: квадрат переместится вправо на 200 пикселей за 500 миллисекунд. Следующее нажатие вернет квадрат на исходную позицию. На время движения кнопка отключается, поэтому циклы не накладываются.

Не добавляйте CSS-переход к этому примеру: координатами уже управляет скрипт. Обработчик не читает размеры элемента после каждой записи стиля, поэтому не создает лишнее чередование чтения и записи геометрии.

JavaScript-вычисления работают в основном потоке и могут задерживать кадры. Используйте покадровый расчет, когда траектория зависит от программной логики. Для обычной смены состояния достаточно CSS-перехода.

Аппаратное ускорение и will-change

Браузер может хранить содержимое элемента на отдельном композиционном слое. При движении такого слоя он меняет положение подготовленного изображения. Это сокращает повторную прорисовку содержимого на каждом кадре.

Chrome, Safari, Firefox и Edge используют композитинг при отрисовке страниц. Однако translate3d() не гарантирует отдельный слой или выполнение всей работы на GPU. На решение влияют содержимое элемента, другие эффекты и доступные ресурсы.

Прием translate3d(0, 0, 0) используют как старый способ подтолкнуть браузер к выделению слоя. Для явной подсказки существует свойство will-change:

.item.is-preparing {
    will-change: transform;
}

Добавьте класс is-preparing перед ожидаемым движением, чтобы браузер успел подготовиться. Удалите его после завершения эффекта, если движение больше не ожидается. Переход можно отслеживать событием transitionend.

Не назначайте will-change: transform всем элементам через универсальный селектор. Дополнительные слои расходуют память и могут ухудшить производительность. Сначала измерьте анимацию без подсказки, затем сравните результат после ее добавления.

Документация MDN по will-change объясняет ограничения этой подсказки. Браузер сохраняет право выбирать собственную стратегию оптимизации.

Как проверить плавность в DevTools

Оценивайте движение по записи работы браузера, а не только визуально. Однократный layout при загрузке страницы нормален. Ищите повторяющиеся вычисления именно во время перемещения.

  1. Откройте страницу с примером в Chrome и через контекстное меню страницы откройте инструменты разработчика.
  2. Выберите панель «Performance» и запустите запись кнопкой Record с круглым значком. Во время записи вызовите движение элемента.
  3. Остановите запись и выделите участок времени с анимацией. На дорожке основного потока найдите события «Layout» и «Paint».
  4. Откройте панель «Rendering» через меню дополнительных инструментов DevTools. Включите «Paint flashing», чтобы увидеть области повторной прорисовки.
  5. В той же панели включите «Layer Borders». Границы покажут композиционные слои, но сами по себе не докажут плавность.

Для композиционной анимации во время движения не должно быть повторного layout из-за самого преобразования. Содержимое слоя также не должно перерисовываться на каждом кадре.

Если подсветка постоянно охватывает движущийся элемент, изучите сопутствующие эффекты. Например, одновременно меняющийся фон может вызывать paint независимо от transform. Отключайте эффекты по одному и повторяйте запись.

Если layout и paint отсутствуют, проверьте длительные JavaScript-задачи на основном потоке. Само преобразование не устраняет задержки от другого кода. Повторите проверку на мобильном устройстве, для которого предназначен интерфейс, затем выключите диагностическую подсветку.

Готовый пример плавного перемещения по наведению

Этот фрагмент объединяет устойчивую область наведения, переход и поддержку клавиатуры. Сохраните код в файл motion.html и откройте его браузером:

<button type="button" class="motion-button">
    <span class="motion-button__label">Наведите курсор</span>
</button>

<style>
.motion-button {
    display: inline-block;
    margin: 40px;
    padding: 16px 24px;
    border: 2px solid royalblue;
    border-radius: 8px;
    background: white;
    color: #172554;
    font: inherit;
    cursor: pointer;
}

.motion-button__label {
    display: inline-block;
    transform: translate3d(0, 0, 0);
    transition: transform .2s ease-in-out;
}

.motion-button:hover .motion-button__label,
.motion-button:focus-visible .motion-button__label {
    transform: translate3d(0, -8px, 0);
}

.motion-button:focus-visible {
    outline: 3px solid #172554;
    outline-offset: 4px;
}

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

    .motion-button:hover .motion-button__label,
    .motion-button:focus-visible .motion-button__label {
        transform: none;
    }
}
</style>

Наведите курсор на кнопку: надпись поднимется на 8 пикселей. Уберите курсор, и надпись вернется за 0,2 секунды. Нажатие Tab переведет фокус на кнопку и запустит такой же эффект.

Медиазапрос prefers-reduced-motion: reduce учитывает системную настройку уменьшения движения. В этом примере он полностью отключает смещение, сохраняя рамку фокуса.

Выбирайте transition для смены состояния и @keyframes для повторяемой последовательности. Подключайте requestAnimationFrame(), если программа рассчитывает координаты во время движения.

Совет от нашей редакции: добавляйте will-change только после замера. Если переход уже обходится без повторных layout и paint, дополнительная подсказка может не принести пользы.

Комментарии

Оставьте свой комментарий
Пока никто не оставил комментариев