Control a CSS animation with XState

The Notification Center example takes inspiration from the React Toastify library.

When you trigger a notification with a timeout with React Toastify, it displays a progress bar at the bottom of the notification. When you hover the notification, the progress bar and the timer are paused, as shown below:

The animation of React Toastify's animations pauses when hovering it

React Toastify uses a CSS animation, defined with a @keyframes. The Notification Center does too:

@keyframes progress-bar {
0% {
transform: scaleX(1);
100% {
transform: scaleX(0);

The animation is applied to the progress bar with CSS:

// ...
animationName: "progress-bar",
animationFillMode: "forwards",
animationIterationCount: 1,
animationTimingFunction: "linear",
transformOrigin: "left",

React Toastify’s implementation’s beauty is using a CSS animation as a resumable timer. A CSS animation can be paused with the animation-play-state property. The Notification Center uses the current state of the machine to determine when to stop the animation:

animationDuration: state.context.timeout! + "ms",
state.matches({ "Waiting for timeout": "Active" }) === true
? "running"
: "paused",

We can know when the animation ends thanks to the animationend event received by the animated element and close the notification:

onAnimationEnd={() => {
type: "animation.end",

The Notification Center example could have used a JavaScript timer, but it would have involved more code and increased complexity. Relying on the browser led to a simple and robust solution. The animation can even be stopped in the Dev Tools without causing any bugs.

Check out the Notification Center example to play with the animation. Hover over it to pause the animation.

Get news from XState by Example

Sign up for the newsletter to be notified when more machines or an interactive tutorial are released. I respect your privacy and will only send emails once in a while.