DEV Community

Cooty
Cooty

Posted on • Updated on

How to make a parameterized spinner component in React

Since the dawn of web2.0 ajax-loaders have become the standard way of telling the user to wait a bit. Back in the day we used GIFs, but now of days it's more common to implement such spinners in CSS and HTML or SVG, this of course makes them more lightweight, but the other advantage is that we can customize them from code, without creating another graphic asset.

In this post I'll show you a way to make an encapsulated spinner component with parameterized size and color attributes.

I'm going to implement the component using React and CSS modules, but of course you can use any other component framework or styling technique, as it's not really bound to React, you can even do this with some server-side templating language I guess. But let's get started.

Step One: Building the basic spinner

First of we'll setup a static spinner that has no customizable styles and we'll take it from there.

Check the working demo below on Stackblitz or follow along by checking the commits on Github.

I won't go too much into the details of the CSS HTML, since we're more concerned about how to make this all work with parameters, but the spinner is basically the inner-most <div> element (the one without a className).

{
  /* outer container */
}
<div className={styles.spinner}>
  {/* inner container */}
  <div className={styles.spinnerAnimationContainer}>
    {/* actual spinner */}
    <div />
  </div>
</div>;
Enter fullscreen mode Exit fullscreen mode

This element gets the animation defined in the spin keyframes at the start of Spinner.module.css.

@keyframes spin {
  0% {
    transform: translate(-50%, -50%) rotate(0deg);
  }
  100% {
    transform: translate(-50%, -50%) rotate(360deg);
  }
}
Enter fullscreen mode Exit fullscreen mode

Also note the how we reset the top border of the div to create the "cut out" from it's border, without this we would only see an outlined circle which is spinning around but we wouldn't see anything moving.

.spinnerAnimationContainer div {
  position: absolute;
  width: 24px;
  height: 24px;
  border: 4px solid rgb(39, 39, 158); /* create the outline */
  border-radius: 50%; /* make it a circle */
  border-top-color: transparent; /* exclude the top border, creating the cut */
}
Enter fullscreen mode Exit fullscreen mode

So now we have a basic spinner, but it's size and color are fixed. Our aim is to make these adjustable via params.

Step Two: Refactoring to CSS variables

Let's start by identifying the values that need to be changed in order to change the overall appearance of the spinner.

First of there's the width and height of the outer container. Since these two values are the same (it's a square technically...) we can outsource this to a custom property (aka variable).

.spinner {
  --spinner-size: 40px;
  width: var(--spinner-size);
  height: var(--spinner-size);
  display: inline-block;
  overflow: hidden;
}
Enter fullscreen mode Exit fullscreen mode

Note💡: The custom properties should be defined on the root element of the component so they'll be also accessible for the child elements.

Now if you start changing the value of --spinner-size, you'll notice that this alone will not change anything. If you change it to a smaller number, say 30px you'll notice that the spinner gets cropped.

Adding a smaller value will crop the spinner - not what we want

If on the other hand you change it to a larger number you'll see that nothing changed, but if you add an outline to the outer container, you'll see that the spinner is no longer centered inside the rectangle.

Adding a larger value will change the offset - also not what we want

Apparently some other values have to be modified to change the appearance.

Notice the lines of CSS responsible for the top and left offset of the absolutely positioned spinner.

.spinnerAnimationContainer div {
  /*...*/
  top: 20px;
  left: 20px;
}
Enter fullscreen mode Exit fullscreen mode

They seem like they supposed to be exactly the half of the size of the container, which would make perfect sense, since you would need to push it away by exactly 50% of the container's size to center it.

So let's use CSS's calc function to encode this equation as the value.

.spinnerAnimationContainer div {
  --spinner-offset: calc(var(--spinner-size) / 2);
  /*...*/
  top: var(--spinner-offset);
  left: var(--spinner-offset);
}
Enter fullscreen mode Exit fullscreen mode

Now if you change the --spinner-size to something smaller, say 30px the spinner will still get cropped, but if you increase the size and also add an outline to the outer container, you'll see that the spinner already stays in the center (no matter what value you add). So we're making progress. But the size itself is still unchanged, so let's fix that.

Looking much better

If we look closer at the other styles affecting the spinner div you'll see that the size is influenced by the border-width, the width and the height.

.spinnerAnimationContainer div {
  /*...*/
  width: 24px;
  height: 24px;
  border: 4px solid rgb(39, 39, 158);
}
Enter fullscreen mode Exit fullscreen mode

Let's take a look at the border's width first and let's try to find some mathematical relation between it's value and the size of the outer container. 4 and 40. That's exactly 10%.

So let's change that to be a dynamic calculation too.

.spinnerAnimationContainer div {
  --spinner-border-width: calc(var(--spinner-size) * 0.1);
  /*...*/
  border: var(--spinner-border-width) solid rgb(39, 39, 158);
}
Enter fullscreen mode Exit fullscreen mode

You'll see that nothing has changed visually, but the border is the same, so that's good.

Now let's look at how to turn the width and height into a calculation. 24 is exactly the 50% of 40 (the current --spinner-size) plus the border's width.

.spinnerAnimationContainer div {
  --spinner-border-width: calc(var(--spinner-size) * 0.1);
  --spinner-inner-size: calc(
    var(--spinner-size) / 2 + var(--spinner-border-width)
  );
  /*...*/
  width: var(--spinner-inner-size);
  height: var(--spinner-inner-size);
  border: var(--spinner-border-width) solid rgb(39, 39, 158);
}
Enter fullscreen mode Exit fullscreen mode

Now let's start changing the value of --spinner-size to either smaller or greater than 40px. Great success! Now the spinner changes its size and we just have to change one value.

So the CSS part is actually done.

Step Three: Connect CSS to props

So now that the size can be changed by just setting one variable we need to make a link between the CSS and the JS props coming from React. But how to do that without introducing some fancy CSS-in-JS library?

Simple. Outsource that one CSS variable to inline-styles. Like so:

function Spinner({ size = "40px", ...props }) {
  return (
    <div
      className={styles.spinner}
      style={{
        "--spinner-size": size,
      }}
      {...props}
    >
      <div className={styles.spinnerAnimationContainer}>
        <div />
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Voilá! We have the size prop's value passed to styles and it will cascade to all it's descendants and even has a nice default value. Of course we have to remove the hard coded --spinner-size from the Spinner.module.css.

Now we can use it like so:

<Spinner size="80px" />
Enter fullscreen mode Exit fullscreen mode

Making the color of the spinner dynamic is trivial at this point.

function Spinner({ size = "40px", color = "rgb(39, 39, 158)", ...props }) {
  return (
    <div
      className={styles.spinner}
      style={{
        "--spinner-size": size,
        "--spinner-color": color,
      }}
      {...props}
    >
      <div className={styles.spinnerAnimationContainer}>
        <div />
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We of course have to change the hard-coded value in the CSS module to our newly defined custom property.

.spinnerAnimationContainer div {
  /*...*/
  border: var(--spinner-border-width) solid var(--spinner-color);
}
Enter fullscreen mode Exit fullscreen mode

Now we can also add the border-color as a prop.

<Spinner size="80px" color="rgb(255, 0, 0)" />
Enter fullscreen mode Exit fullscreen mode

The whole thing now looks like this.

Of course if passing values as-is seems a bit too flexible to you and you already have some sort of design system with tokens defined, then you can pass those instead and even you can validate the values using TypeScript or React PropTypes.

This could look something like this.

<Spinner size="xl" color="brand-color" />
Enter fullscreen mode Exit fullscreen mode

What we've learned from all this

Needles to say that you can implement such a component in any JS framework, or even with some server-side templating system. The core idea is to use inline CSS custom properties to communicate between the rendered markup (that knows about dynamic data) and CSS. The other core idea is using calc function to define values as simple equations.

I hope you can reuse either the ideas presented here in you own projects and you've learned something new!

Top comments (0)