Обработка событий CSS-переходов в JavaScript

В этой статье мы изучим какие события возникают в DOM при CSS-переходах и как их можно использовать в JavaScript. Рассмотрим несколько примеров на эту тему.
CSS-переходы и их события
Как вы уже знаете для создания простой анимации в CSS можно использовать переходы. Суть здесь заключается в том, что с помощью них мы можем указать браузеру то, что значение какого-то CSS-свойства нужно изменить не сразу, а плавно в течении некоторого количества времени. После этого, для того чтобы переход начался, нам нужно будет всего лишь изменить значение этого CSS-свойства, и браузер сам выполнит эту анимацию.
Очень часто на сайтах и в веб-приложениях запуск CSS-перехода выполняется при наступлении каких-то определенных событий. Например, при клике на элемент или при наведении на него курсора.
Для задания CSS-перехода используются следующие свойства:
transition-property
– для указания CSS-свойства, которое нужно анимировать;transition-duration
– длительность перехода;transition-timing-function
– задает как должна изменяться скорость во время CSS-перехода;transition-delay
– время задержки перед началом перехода.
Например, мы хотим, чтобы изменения цвета фона (background-color
) происходило плавно в течение 1 секунды. При этом нам нужно, чтобы этот процесс начинался не сразу, а через 0,1 секунды после создания CSS-перехода. Кроме этого, необходимо чтобы переход начинался и заканчивался медленно, или другими словами, чтобы анимация выполнялась посредством функции ease-in-out
:
<style>
.box {
background-color: #fffde7;
transition: background-color;
transition-duration: 1s;
transition-timing-function: ease-in-out;
transition-delay: 0.1s;
}
.box:hover {
background-color: #ffeb3b;
}
</style>
<div class="box">Lorem</div>
В CSS также можно записать все эти свойства кратко, используя transition
:
.box {
background-color: #fffde7;
transition: background-color 1s ease-in-out 0.1s;
}
При CSS-переходах могут возникать следующие события в DOM:
transitionrun
– при создании CSS-перехода, то есть до того, как он реально начался;transitionstart
– когда CSS-переход фактически начался;transitionend
– при завершении выполнения перехода;transitioncancel
– при отмене CSS-перехода.
Событие transitionrun
возникает при создании CSS-перехода, то есть перед transition-delay
. В то время transitionstart
возникает, когда CSS-переход уже начался, то есть сразу после окончания transition-delay
.
Событие transitionend
возникает после завершения CSS-перехода. Если CSS-переход удаляется до завершения, то событие transitionend
сгенерировано не будет. Например, если мы во время перехода удалили свойство transition
или установили для элемента свойство display
со значением none
.
При удалении анимации до её завершения генерируется другое событие, а именно transitioncancel
.
Добавление обработчиков к этим событиям осуществляется как обычно:
el.addEventListener('transitionend', () => {
// действия, которые нужно выполнить при возникновении события transitionend на элементе el
});
Здесь мы добавили обработчик события transitionend
с помощью метода addEventListener
.
Пример 1. Плавное изменение цвета фона элемента
В этом примере создадим HTML-элемент .box
. При нажатии на который мы будем с помощью JavaScript менять цвет его фона на случайный. Но изменение цвета будем производить не мгновенно, а плавно с помощью CSS-перехода:

HTML-код элемента:
<div class="box">#f2c2b4</div>
Стили, с помощью которых мы зададим для .box
определенные размеры, цвет фона, CSS-переход, а также некоторые другие вещи:
.box {
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 100px;
margin: 20px;
color: black;
font-size: 18px;
font-family: monospace;
background-color: #f2c2b4;
cursor: pointer;
transition: background-color 0.5s ease-in-out 0.1s;
}
Код JavaScript, написанный с использованием ES6 классов:
class ColorBox {
#el;
#isTransition = false;
constructor(selector) {
this.#el = document.querySelector(selector);
this.#addEventListeners();
}
#addEventListeners() {
this.#el.addEventListener('click', (e) => {
if (this.#isTransition) {
console.log('CSS-переход ещë не закончился', new Date().toLocaleTimeString());
return;
}
this.#isTransition = true;
const hex = `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`;
e.target.style.backgroundColor = hex;
const [r, g, b] = e.target.style.backgroundColor.replace(/[^\d,]/g, '').split(',');
const brightness = Math.floor((r * 299 + g * 587 + b * 114) / 1000);
e.target.style.color = brightness < 128 ? 'white' : 'black';
e.target.textContent = hex;
});
this.#el.addEventListener('transitionrun', () => {
console.log('CSS-переход создан', new Date().toLocaleTimeString());
});
this.#el.addEventListener('transitionstart', () => {
console.log('CSS-переход начался', new Date().toLocaleTimeString());
});
this.#el.addEventListener('transitionend', () => {
this.#isTransition = false;
console.log('CSS-переход закончился', new Date().toLocaleTimeString());
});
}
}
const colorBox = new ColorBox('.box');
В коде мы обрабатываем следующие события, возникающие на элементе .box
:
click
– при клике;transitionrun
– при создании CSS-перехода;transitionstart
– при начале CSS-перехода;transitionend
– при окончании CSS-перехода.
При клике мы проверяем, выполняется ли CSS-переход, то есть истинно ли значение свойства this.#isTransition
. Если так, то выводим сообщение в консоль и прекращаем дальнейшее выполнение функции. В противном случае мы устанавливаем приватному свойствуthis.#isTransition
значение true
.
Далее мы генерируем случайный цвет и сохраняем его в переменную hex
. После этого присваиваем CSS-свойству background-color
значение hex
. Затем мы объявляем переменные r
, g
и b
и сохраняем в них компоненты соответственно красного, зеленого и синего цветов. Они нам понадобятся для определения яркости цвета и установки в зависимости от этого для текста черного или белого цвета. В конце с помощью textContent
мы выведем текст цвета в формате HEX в качестве содержимого .box
.
В обработчиках событий transitionrun
, transitionstart
и transitionend
мы будем просто выводить соответствующее сообщение в консоль. Кроме этого в transitionend
будем ещё дополнительно устанавливать приватному свойству this.#isTransition
значение false
, которое будет говорить о том, что CSS-переход завершился. И можно создавать новый при клике на .box
.
Пример 2. Плавное перемещение элемента
В этом примере напишем код для плавного перемещения элемента с использованием translateX
и transition
:

Для этого нам понадобится вот такой HTML-код:
<div class="box-wrapper container">
<div class="box"></div>
</div>
<div class="controls container">
<button type="button" class="start">start</button>
<button type="button" class="stop" disabled>stop</button>
</div>
<div class="logs container"></div>
Здесь .box-wrapper
– это контейнер внутри которого мы будем плавно двигать элемент .box
с помощью transform: translateX(300px)
в течение 3 секунд. Запускать это действие будем при клике по кнопке «start», а в JavaScript в обработчике этого события будем добавлять к .box
класс translating
. Прервать анимацию, можно будет при клике по кнопке «stop».
В элемент .logs
мы будем помещать события, связанные с CSS-переходом в порядке их возникновения.
Для оформления, а также для установки transform
и transition
напишем следующий CSS-код:
.container {
width: 400px;
margin-right: auto;
margin-bottom: 1rem;
margin-left: auto;
}
.box-wrapper {
background-color: #dcedc8;
}
.box {
width: 100px;
height: 100px;
background-color: #8bc34a;
}
.translating {
transform: translateX(300px);
transition: transform 3s ease-in-out 0.5s;
}
.controls {
margin-bottom: 1rem;
text-align: center;
}
.logs {
min-height: 100px;
padding: 20px 15px;
font-size: 14px;
font-family: monospace;
background-color: #fff9c4;
}
JavaScript-код:
const elBox = document.querySelector('.box');
const elLogs = document.querySelector('.logs');
const btnStart = document.querySelector('.start');
const btnStop = document.querySelector('.stop');
btnStart.onclick = () => elBox.classList.add('translating');
btnStop.onclick = () => elBox.classList.remove('translating');
elBox.addEventListener('transitionrun', (e) => {
btnStart.disabled = true;
const now = new Date().toLocaleTimeString();
const html = `<div>> <b>${e.type}</b> ${e.propertyName} ${now}</div>`;
elLogs.innerHTML = html;
});
elBox.addEventListener('transitionstart', (e) => {
btnStop.disabled = false;
const now = new Date().toLocaleTimeString();
const html = `<div>> <b>${e.type}</b> ${e.propertyName} ${now}</div>`;
elLogs.insertAdjacentHTML('beforeend', html);
});
elBox.addEventListener('transitionend', (e) => {
btnStart.disabled = false;
btnStop.disabled = true;
const now = new Date().toLocaleTimeString();
const elapsedTime = Math.round(e.elapsedTime * 100) / 100;
const html = `<div>> <b>${e.type}</b> ${e.propertyName} ${elapsedTime}s ${now}</div>`;
elLogs.insertAdjacentHTML('beforeend', html);
});
elBox.addEventListener('transitioncancel', (e) => {
btnStart.disabled = false; btnStop.disabled = true;
const now = new Date().toLocaleTimeString();
const elapsedTime = Math.round(e.elapsedTime * 100) / 100;
const html = `<div>> <b>${e.type}</b> ${e.propertyName} ${elapsedTime}s ${now}</div>`;
elLogs.insertAdjacentHTML('beforeend', html);
});
Что делает этот код? При клике по кнопке .start
мы будем добавлять к элементу .box
класс translating
и тем самым создавать CSS-переход. При клике по .stop
убираем у элемента .box
класс translating
и тем самым удаляем CSS-переход.
Остальные обработчики событий просто выводят сообщения в элемент .logs
со следующей информацией:
type
– тип события;propertyName
– имя CSS-свойства для перехода;elapsedTime
– количество времени в секундах, в течение которого выполнялся переход.
Пример 3. Слайдер с плавными переходами
В этом примере мы создадим простой слайдер с зацикливанием или иными словами с бесконечной прокруткой.

Начнём с написания HTML-кода:
<div class="slider-wrapper">
<div class="slider">
<div class="slider-item slider-item-active">1</div>
<div class="slider-item">2</div>
<div class="slider-item">3</div>
<div class="slider-item">4</div>
</div>
</div>
<div class="slider-controls">
<button type="button" class="slider-prev">prev</button>
<button type="button" class="slider-next">next</button>
</div>
Здесь .slider-item
– это слайд. В этом слайдере их 4. Для смены слайдов будем использовать кнопки .slider-prev
и .slider-next
.
Для размещения элементов в слайдере будем использовать технологию CSS Flexbox. В качестве ширины установим .slider-item
значение 200px
:
.slider-wrapper {
width: 200px;
overflow-x: hidden;
}
.slider {
display: flex;
}
.slider-item {
flex: 0 0 200px;
height: 150px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
}
Смещать плавно слайды будем с помощью CSS-функции translateX
и transition
. Здесь мы будем их устанавливать посредством JavaScript:
class Slider {
constructor(selector) {
this.slider = document.querySelector(selector);
this.elFirst = this.slider.firstElementChild;
this.elLast = this.slider.lastElementChild;
this.btnPrev = document.querySelector('.slider-prev');
this.btnNext = document.querySelector('.slider-next');
this.direction = 'next';
this.width = this.slider.parentElement.getBoundingClientRect().width;
this.count = this.slider.querySelectorAll('.slider-item').length;
this.step = this.width * this.count;
this.elLast.style.transform = `translateX(${-this.step}px)`;
this.orders = [...this.slider.children].map((value, index) => index);
this.orders = [...this.orders.slice(-1), ...this.orders.slice(0, -1)];
this.addEventListener();
}
move() {
let value = new WebKitCSSMatrix(this.slider.style.transform).m41;
value += this.direction === 'next' ? -this.width : this.width;
this.slider.style.transform = `translateX(${value}px)`;
this.slider.style.transition = 'transform 1s ease';
const el = this.slider.querySelector('.slider-item-active');
let elNew = this.direction === 'next' ? el.nextElementSibling : el.previousElementSibling;
if (!elNew) {
elNew = this.direction === 'next' ? this.elFirst : this.elLast;
}
el.classList.remove('slider-item-active');
elNew.classList.add('slider-item-active');
}
addEventListener() {
this.btnPrev.onclick = () => {
this.direction = 'prev';
this.move();
}
this.btnNext.onclick = () => {
this.direction = 'next';
this.move();
}
this.slider.ontransitionrun = () => {
this.btnPrev.disabled = true;
this.btnNext.disabled = true;
const index = this.direction === 'next' ? this.orders[0] : this.orders[this.orders.length - 1];
const el = this.slider.querySelector(`.slider-item:nth-child(${index + 1})`);
let value = new WebKitCSSMatrix(el.style.transform).m41;
value += this.direction === 'next' ? this.step : -this.step;
el.style.transform = `translateX(${value}px)`;
if (this.direction === 'prev') {
this.orders = [...this.orders.slice(-1), ...this.orders.slice(0, -1)];
} else {
this.orders = [...this.orders.slice(1), ...this.orders.slice(0, 1)];
}
}
this.slider.ontransitionend = () => {
this.btnPrev.disabled = false;
this.btnNext.disabled = false;
}
}
}
const slider = new Slider('.slider');
Из интересного здесь то, что порядок следования элементов мы пишем в массив this.orders
: [3, 0, 1, 2]
. Который затем используем для того, чтобы организовать бесконечную прокрутку слайдера. При движении вправо, нам нужно крайний элемент слева переместить после последнего. А при движении влево, наоборот, крайний элемент справа переместить перед первым.
С помощью this.orders
получить такие элементы очень легко:
this.orders[0]
– крайний элемент слева;this.orders[this.orders.length - 1]
– крайний элемент справа.
Перемещение крайнего элемента для зацикливания мы делаем при создании CSS-перехода, то есть в обработчике события ontransitionrun
. Здесь мы также переводим кнопки в неактивное состояние:
this.btnPrev.disabled = true;
this.btnNext.disabled = true;
После завершения CSS-перехода делаем их опять активными:
this.slider.ontransitionend = () => {
this.btnPrev.disabled = false;
this.btnNext.disabled = false;
}