How to make a parameterized spinner component in React
Tags: react, css, tutorial, html
Published: 2023-01-14 21:43:54

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](https://github.com/Cooty/react-parameterized-spinner/tree/66e5586b145e153f2da285800a070d0213174c02).

{% embed https://stackblitz.com/edit/vitejs-vite-a2e2jg?embed=1&file=src/components/spinner/Spinner.jsx %}

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`).

```jsx
{
  /* outer container */
}
<div className={styles.spinner}>
  {/* inner container */}
  <div className={styles.spinnerAnimationContainer}>
    {/* actual spinner */}
    <div />
  </div>
</div>;
```

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

```css
@keyframes spin {
  0% {
    transform: translate(-50%, -50%) rotate(0deg);
  }
  100% {
    transform: translate(-50%, -50%) rotate(360deg);
  }
}
```

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.

```css
.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 */
}
```

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).

```css
.spinner {
  --spinner-size: 40px;
  width: var(--spinner-size);
  height: var(--spinner-size);
  display: inline-block;
  overflow: hidden;
}
```

> 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ie1zchjzx5j9zww8pq7j.jpg)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hsyv0j69m9vlbzs43wks.jpg)

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.

```css
.spinnerAnimationContainer div {
  /*...*/
  top: 20px;
  left: 20px;
}
```

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.

```css
.spinnerAnimationContainer div {
  --spinner-offset: calc(var(--spinner-size) / 2);
  /*...*/
  top: var(--spinner-offset);
  left: var(--spinner-offset);
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tq4w2dpob211cpzr2h8x.jpg)

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`.

```css
.spinnerAnimationContainer div {
  /*...*/
  width: 24px;
  height: 24px;
  border: 4px solid rgb(39, 39, 158);
}
```

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.

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

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.

```css
.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);
}
```

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](https://github.com/Cooty/react-parameterized-spinner/tree/5e3340b3dac6e0a4e04a19e163676972866c21b2).

{% embed https://stackblitz.com/edit/vitejs-vite-jxlcib?embed=1&file=src/components/spinner/Spinner.module.css %}

## 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:

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

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:

```jsx
<Spinner size="80px" />
```

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

```jsx
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>
  );
}
```

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

```css
.spinnerAnimationContainer div {
  /*...*/
  border: var(--spinner-border-width) solid var(--spinner-color);
}
```

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

```jsx
<Spinner size="80px" color="rgb(255, 0, 0)" />
```

The whole thing now looks [like this](https://github.com/Cooty/react-parameterized-spinner/tree/7dc8d32623dfd275e617289ed981185059d78de5).

{% embed https://stackblitz.com/edit/vitejs-vite-b8rjwh?embed=1&file=src/components/spinner/Spinner.jsx %}

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](https://spectrum.adobe.com/page/design-tokens/) defined, then you can pass those instead and even you can validate the values using TypeScript or React [PropTypes](https://reactjs.org/docs/typechecking-with-proptypes.html).

This could look something like this.

```jsx
<Spinner size="xl" color="brand-color" />
```

## 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!