How to stop your spinner from jumping in React
Tags: react, javascript, css, ux
Published: 2019-08-22 10:17:06

---
title: How to stop your spinner from jumping in React
published: true
description: Want to improve your spinner game? This article shows you how to spin like you've never spun before
tags: react, javascript, css, ux
cover_image: https://images.unsplash.com/photo-1482614312710-79c1d29bda2a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1440&q=80
canonical_url: https://www.selbekk.io/blog/2019/08/how-to-stop-your-spinner-from-jumping-in-react/
---

Sometimes, when loading data in a web app, it happens in this waterfall-y approach. First, you fetch some auth data, then some user data, and finally the data required to build your view.

This can often lead to different spinners being rendered in the same place, leading to the following user experience:

{% codesandbox jumping-spinners-yubc0 %}

See how that spinner kind of "jumps" back to start every time the text changes? *I hate that!* Granted, this issue will probably disappear once we can use [Suspense](https://css-tricks.com/reacts-experimental-suspense-api-will-rock-for-fallback-ui-during-data-fetches/) for everything - but until then I'd love to fix this for our customers.

This "jump" happens because a new spinner is mounted to our DOM, and the CSS animation is started anew. 

A few weeks ago, React Native DOM author [Vincent Reimer](https://twitter.com/vincentriemer) posted this little demo:

{% twitter 1160670658512052224 %}

I was amazed! 🤩 Is this even a possibility? How would you even do that?

After staring in bewilderment for a few minutes, I started digging into how this could be achieved. And as it turns out, it's a pretty simple trick!

## How to sync your spinners

The moving parts of spinners are typically implemented with [CSS animations](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations). That's what I did in the example above, at least. And that animation API is pretty powerful. 

The `animation-delay` property is typically used to orchestrate CSS animations, or stagger them one after another (first fade in, then slide into place, for example). But as it turns out, it can be used to rewind the animation progress as well - by **passing it negative values**!

Since we know how long our spinner animation loop is, we can use negative `animation-delay` values to "move" the animation to the correct spot when our spinner mounts.

Given the following CSS:

```css
keyframe spin {
  to { transform: rotate(360deg); }
}
.spinner {
  animation: 1000ms infinite spin;
  animation-delay: var(--spinner-delay);
  /* visual spinner styles omitted */
}
```

We can set the animation delay when our spinner component mounts:

```js
const Spinner = (props) => {
  const mountTime = React.useRef(Date.now()));
  const mountDelay = -(mountTime.current % 1000);

  return (
    <div 
      className="spinner" 
      aria-label="Please wait" 
      style={{ '--spinner-delay': `${mountDelay}ms` }}
    />
  );
};
```

Here, we use React's `useRef` hook to save the point in time our `Spinner` component mounted. We then calculate the amount of milliseconds to "rewind" our spinner animation, and make that value negative.

Finally, we pass down the `--spinner-delay` CSS custom property via a style prop.

Here's the result:

{% codesandbox non-jumping-spinners-ojkjr %}

## More detail please

If you want a step-by-step on what happens here? No worries, here it is. In excruciating detail. 🙈

```js
const mountTime = React.useRef(Date.now()));
```

The function `Date.now()` returns the amount of milliseconds from January 1st, 1970 ([see here](https://stackoverflow.com/questions/2533563/why-are-dates-calculated-from-january-1st-1970/2533567) for a deeper dive into why that is). We're going to use that number as a baseline for where our animation will be when it mounts.

The `React.useRef` hook lets you save an arbitrary value without triggering a re-render. It's perfect for saving stuff like our "mount time". You can see [the documentation](https://reactjs.org/docs/hooks-reference.html#useref)) for more details about this function.

```js
const mountDelay = -(mountTime.current % 1000);
```

The `mountDelay` constant is the actual number of milliseconds we're going to "rewind" our animation. The number `1000` must match the amount of milliseconds the animation runs for - so if your spinner spins slower or quicker than the one in this example, you will have to adjust this number.

We're accessing the value calculated in `mountTime` by accessing the `current` property of the `mountDelay` ref. This is how React refs are structured.

We're using the modulo operator `%` to figure out how many milliseconds out into our animation we are. If you're not familiar with the `%` operator, that's fine. If you do `1123 % 1000`, you get 123. If you do `15 % 15`, you get 0. You can read more about it [here](https://riptutorial.com/javascript/example/760/remainder---modulus----). 

Finally, we're negating the number, since we want a negative delay value to pass into the `animation-delay` property.

```js
<div style={{ '--spinner-delay': `${mountDelay}ms` }} />
```

Did you know you can pass in CSS custom properties (formerly known as CSS variables) to your classes via the `style` prop? Yeah, me neither! Turns out, that's actually a pretty nifty technique to pass dynamic values to our CSS. Note that we're suffixing our millisecond value with `ms` before passing it in. 

You can read more about custom properties on [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties). 

```css
keyframe spin {
  to { transform: rotate(360deg); }
}
.spinner {
  animation: 1000ms infinite spin;
  animation-delay: var(--spinner-delay);
}
```

In our CSS, we specify our animation via the `animation` property, and then we specify the `animation-delay` value separately. You could do this in the `animation` declaration as well, but this is a bit more readable to me.

And that's it!

I hope you use this technique to improve your spinners, and share it with your friends. Thanks for reading 👋