Need mock data for your designs or tests? Get it instantly with no setup required!
How to Listen for CSS Events in Javascript
A guide to working with CSS animation and transition events.
When building complex UIs, sometimes you might want to run a Javascript function based on the current state of an interaction in CSS. For example, you may want to know when an animation starts before beginning a timer. Or when a transition ends, you could send some analytic data. Fortunately, there are several events we can hook into for both animations and transitions that make scenarios like these easy.
In this article, you'll learn how to detect CSS animation events in Javascript. The second part will focus on detecting CSS transition events.
A note about browser compatibility: Most of these events are supported across browsers, but some are more experimental or inconsistent. Please refer to the MDN links or caniuse.com for more complete information.
CSS Animation Events
The HTMLElement interface has four different CSS animation events to be used in combination with addEventListener
.
All of these events can be accessed via property name as well, beginning with
on
(e.g.onanimationstart
oronanimationend)
.
Animationstart
Let's begin with animationstart. This event works just as its name implies and runs as soon as an animation starts. It will take into account any animation-delay
value that's set and waits until that has elapsed before running.
// element is an HTML element with a CSS animation property
const element = document.querySelector(".an-element");
element.addEventListener("animationstart", () => {
console.log('animation has started');
});
Animationend
Next we have the animationend event that runs when an animation ends. It does not run if the animation property is removed from the element or if the element is removed from the DOM. If that's the case, the animationcancel
event would run instead.
element.addEventListener("animationend", () => {
console.log('animation has ended');
});
Animationcancel
The animationcancel event runs if an animation is canceled or aborted. As mentioned above, this could happen if the element is removed from the DOM or if the animation property is removed.
element.addEventListener("animationcancel", () => {
console.log('animation was cancelled');
});
Animationiteration
The animationiteration event runs when an iteration of an animation ends and another begins, via the animation-iteration-count
CSS property. For example, if you have an animation that repeats three times, the event would run twice: once for each ending and beginning.
element.addEventListener("animationiteration", () => {
console.log('animation has iterated once');
});
Now let's use these in an example. We'll build an interaction that updates a counter and some text using event listeners with CSS animation.
Start with the following HTML. This defines some buttons and markup to use.
<div class="buttons">
<button class="play">Play</button>
<button class="cancel">Cancel</button>
</div>
<div class="container">
<div class="circle"></div>
<p class="message"></p>
</div>
Now for some CSS.
.container {
max-width: 500px;
margin: auto;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.message {
color: #333;
text-align: center;
margin-top: 40px;
}
.circle {
display: flex;
justify-content: center;
align-items: center;
height: 150px;
width: 150px;
background: linear-gradient(
90deg,
hsla(148, 89%, 78%, 1) 0%,
hsla(210, 81%, 22%, 1) 100%
);
color: #fff;
font-size: 60px;
border-radius: 90px;
opacity: 0;
transition: opacity 1s ease;
}
.circle.play {
animation: expandCollapse 7s ease;
animation-iteration-count: 3;
}
@keyframes expandCollapse {
0% {
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.4);
}
100% {
transform: scale(0.3);
}
}
We won't go through all of the styles above, but the key thing to note is that we have a .play
class that contains the animation property, which is then added to the circle
div when the play button is clicked.
Next we query for some elements, including the .circle
. We'll also initialize a counter
to increment.
const playButton = document.querySelector(".play");
const cancelButton = document.querySelector(".cancel");
const message = document.querySelector(".message");
const circle = document.querySelector(".circle");
let counter;
Now we need a click event for the play button. It will start the animation by adding the play
class and setting the counter value to 3
.
playButton.addEventListener("click", () => {
circle.classList.add("play");
counter = 3;
});
Let's finally add our animation event listeners.
circle.addEventListener("animationstart", () => {
circle.innerText = counter;
message.innerText = "Breathe";
});
When the animation starts, we want to update the circle to display the counter value and set some message text.
circle.addEventListener("animationiteration", () => {
--counter;
circle.innerText = counter;
message.innerText = "Keep breathing";
});
When iterating, decrement the counter value and update the message.
circle.addEventListener("animationend", () => {
circle.innerText = "";
circle.classList.remove("play");
message.innerText = "Have a great day!";
});
When the animation ends, remove both the play
class and the counter value, and then display some final message text.
One last thing to demonstrate is canceling the animation. To do that, we can add a click event to the cancelButton
to remove the circle element from the DOM.
cancelButton.addEventListener("click", () => {
circle.remove();
});
If this occurs while the animation is running, the animationcancel
event will fire. We can then update our messaging to show an error message.
circle.addEventListener("animationcancel", () => {
message.innerText = "There was an error 😟";
});
CSS Transition Events
Now that we've covered animation events, we can move on to CSS transition events. The overall idea is similar.
Transitionstart
The transitionstart event runs as soon as the actual transition starts, and after any transition-delay
value has elapsed.
const element = document.querySelector(".an-element");
element.addEventListener("transitionstart", () => {
console.log('transition has started');
});
Transitionend
The transitionend event will run when a transition ends in both directions: on completion and also when reverting back to the initial state. For example, imagine hovering over a button that has a one-second transition of the background color. transitionend
would run both when the transition ends while hovering on, and at the end of the transition while hovering off.
element.addEventListener("transitionend", () => {
console.log('transition has ended');
});
There are a few important cases where this event will not run:
- The element being transitioned is removed from the DOM.
- The transition property is removed.
- The element is set to
display: none
at some point during the transition.
Transitionrun
The transitionrun event runs when a transition is created, but before any transition-delay
begins. It's different from transitionstart
in that it will begin before transitionstart
if a delay is applied, and at the same time if there is no delay. Think of it as a way of marking the actual start of a transition.
element.addEventListener("transitionrun", () => {
console.log('transition is running');
});
Transitioncancel
Finally there's transitioncancel, which runs if a transition is cancelled between transitionrun
and before transitionend
.
element.addEventListener("transitioncancel", () => {
console.log('transition was cancelled');
});
Imagine the same button hover example. transitioncancel
will run if you hover over a button with a one-second transition, then hover off before the transition finishes.
It's important to note that if a transition is cancelled, the transitionend
event won't run.
I noticed some inconsistency between different browsers with this event. In Chrome and Safari, I could only get
transitioncancel
to trigger betweentransitionrun
andtransitionstart
. In Firefox, it could be cancelled right up untiltransitionend
runs. You can see this yourself in the demo below across various browsers.
Let's finish what we learned with a quick example. We'll add a CSS transition hover effect to a button to visualize these events.
Start with some basic HTML.
<div class="container">
<button class="btn">Hover</button>
<span class="status">Idle</span>
</div>
Next for some styling.
.container {
max-width: 500px;
margin: auto;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.btn {
background: transparent;
border: 2px solid #2a9d8f;
cursor: pointer;
padding: 16px 36px;
font-size: 24px;
position: relative;
overflow: hidden;
color: #2a9d8f;
}
.btn::after {
content: "";
display: block;
background: #9bded6;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
transform: translateY(100%);
transition: transform 2s ease;
transition-delay: 1s;
z-index: -1;
}
.btn:hover::after {
transform: translateY(0);
}
.status {
display: block;
margin-top: 20px;
}
The important parts here are the transition and the transition-delay. We learned above that the presence of a delay determines when the transitionstart event actually starts. For our demo, we're adding it to get a more noticeable difference between events.
Now that we have a button, let's start listening for events.
First, query for the button and the status span.
const button = document.querySelector(".btn");
const status = document.querySelector(".status");
Next, add the event listeners for transitionrun
, transitionstart
, transitionend
and transitioncancel
. When each of them run, update the innerText
value of the status span.
button.addEventListener("transitionrun", () => {
status.innerText = "Running";
});
button.addEventListener("transitionstart", () => {
status.innerText = "Started";
});
button.addEventListener("transitionend", () => {
status.innerText = "Ended";
});
button.addEventListener("transitioncancel", () => {
status.innerText = "Cancelled";
});
Now if you experiment by hovering over the button, you should see all four events working!
Conclusion
While writing this article, I was reminded just how useful these animation and transition events can be. What we saw in the examples above could have probably been done by setting a series of setTimeout
functions that line up with the animation timing, but reacting to the events themselves can keep our code much more straightforward and synchronized.