100 lines of code, 4 rectangles, 2 circles, 1 React SVG Loading Spinner
Blog

100 lines of code, 4 rectangles, 2 circles, 1 React SVG Loading Spinner

100 lines of code, 4 rectangles, 2 circles, 1 React SVG Loading Spinner

No one wants to make their users wait. What you want, is to provide the best experience possible like with a custom Loading component for example.

I really like the spinning loader on Meteor Development Group products. So much so that I actually dug into it to see how it was built. I found that it was built with a React component generating DOM elements (source code), linked to a bunch of complex, though super smart, CSS code (pre-processed with LESS (source code).

MDG Spinner animated GIF

This post is a step-by-step tutorial to create a re-usable Loading React component as cool as the one shown above rendering controlled SVG elements.

THE AWESOME SPINNING LOADER YEAH

Don’t know SVG? No worries! 😄 I’ll show you to manipulate shapes like circle and rectangles, as well as walk you through adding some cool effects like dashed borders and gradients. All that with JSX! ⚛️

Our playground: an <svg /> tag

The core of the code we’ll write will take place inside an <svg /> tag:

const LoadingSpinner = () => {
  return (
    <svg viewBox="0 0 120 120">
      {/* the shapes making the spinner */}
    </svg>
  );
}

This <svg /> tag is our playground, whose area is controlled by a viewBox attribute. In our case a square of 120x120 — this is all we need to start creating our loading spinner. If you want to learn more about the viewBox attribute, I highly recommend reading this excellent article by Sara Souedian.

The core shapes, or when the foundations lie on 2 circles

Let’s draw the base shapes of the loader using the <circle /> element:

A <circle /> element needs a radius along with X & Y coordinates of its center.

For example, we can draw a circle in the center of our component with a radius of 42 like so:

Screenshot of a simple circle

To get to something closer to the MDG spinner, let’s add another circle, make their backgrounds transparent thanks to the fill attribute. We’ll also customize their border, thanks to the stroke & strokeWidth attributes.

Screenshot of two circles

What’s up with these two shapes? On the one hand, we are missing the dashed border on the outer circle and on the other hand, the inner circle should just show an arc.

To get the outer circle to render as we’d like, we are going to use the strokeDasharray attribute to create a series of dash long by 2 and spaced by 9.5 to get a “nice dashed wheel”:

<circle stroke="#133C99" strokeWidth={6} strokeDasharray="2 9.5" /* ... */ />

Considering the arc, also known as our half-of-the-inner-circle, we’ll hide half of it with a linear gradient: <linearGradient />! Linear gradients are made of stops: a stop defines where and how a color should change. We are going to use 4 stops to create a clear break between an invisible part and a filled part (of blue).

…uh? 🤔 A picture is worth a thousands words, isn’t it? May the GIF below explain what I mean by these 4 stops:

Screencast of 4 stops in a linear gradient

The code to generate such effect looks like this:

<linearGradient id="arc-gradient">
  <stop offset="0%" stopOpacity="0" />
  <stop offset="49.99%" stopOpacity="0" />
  <stop offset="50%" stopColor="#133C99" />
  <stop offset="100%" stopColor="#133C99" />
</linearGradient>

We make the first two <stop /> element transparent thanks to stopOpacity, and right when we are in the middle of the shape, we start with the color we picked for the spinner.

Note that the gradient has an id. The <circle /> will refer to the gradient thanks to this id! 🤝 Something like this:

<circle stroke="url(#arc-gradient)" /* ... */ />

This is what we have so far:

Screenshot of the two base shapes

const LoadingSpinner = () => {
  return (
    <svg viewBox="0 0 120 120">
      <circle r={42} cx={60} cy={60} fill="transparent" stroke="#133C99" strokeWidth={6} strokeDasharray="2 9.5" />
      <linearGradient id="arc-gradient">
        <stop offset="0%" stopOpacity="0" />
        <stop offset="49.99%" stopOpacity="0" />
        <stop offset="50%" stopColor="#133C99" />
        <stop offset="100%" stopColor="#133C99" />
      </linearGradient>
      <circle r={36} cx={60} cy={60} fill="transparent" stroke="url(#arc-gradient)" strokeWidth={2} />
    </svg>  
  );
};

The opacity effect, or when you make magic with 4 rectangles

If you take a closer look at the expected result, you’ll notice that the outer circle seems to step aside while turning. Mmmh… maybe we could apply some kind of gradient and play with the opacity like we have done with the arc? 🌀

This is what we are going to do: this is called an angular gradient… but there is no support for them in SVG land. “What?!” might have been your reaction reading this. No worries, we’ll elmuate the same view with 4 rectangles! 🎉

And SVG elements got us covered here:

// renders a rectangle of 60x60 positioned at 0,0 in relation to the viewBox
<rect x={0} y={0} width={60} height={60} />

I got inspired by this awesome pen by Zachary Skalko where he generates 360 fancy rectangles to create this kind of conical gradient.

Conical gradient by Zachary Skalko

It’s pretty cool, but in our case, we don’t need to hammer the DOM with 360 rectangles, 4 rectangles is just enough for our simple spinner.

Our 4 rectangles will be almost transparent, but just visible enough to smoothly mask the parts of the outer circle we want to disappear. Here is a negative of what it will look like:

Negative of the rectangle gradients

That means we need to create one gradient for each rectangle.

Assuming a rectangle takes 1/4 of the playground, we could describe where we want to go:

There are 4 squares/rectangles on the playground.

A rectangle is a square of 60x 60.

Each rectangle has a gradient associated.

A gradient has a "start opacity" and an "end opacity".

A gradient "starts" where the previous one "ended".

With these assumptions in mind, we could imagine an API for a component:

[/* list of (rectangle + gradient) properties*/].map(
  ({ rectangle, gradient }) => 
    <FancyRectangle 
      x={rectangle.x}
      y={rectangle.y}
      width={60}
      height={60}
      gradientStart={gradient.start}
      gradientEnd={gradient.end}
    />
);

const FancyRectangle = props => (
  /*
    render a <rect /> &
    render a <linearGradient />
  */
);

5 opacity stops going from 100 to 90, 75, 50 and finally 0 feels good. Here is a simple version of the code we need to render the 4 rectangle in our <LoadingSpinner />:

const opticalGradient = [
  // bottom left rectangle 
  {x: 0, y: 60, start: 0.5, end: 0 },
  // top left rectangle 
  {x: 0, y: 0, start: 0.5, end: 0.75 },
  // top right rectangle 
  {x: 60, y: 0, start: 0.75, end: 0.9 },
  // bottom right rectangle 
  {x: 60, y: 60, start: 0.9, end: 1 },
];

const OpticalGradient = props => {
  return props.definitions.map(
    ({ x, y, start, end }) => [
      <linearGradient id={`gradient-${x}-${y}`} key="gradient">
        <stop offset="0%" stopColor="#FFF" stopOpacity={start} />
        <stop offset="100%" stopColor="#FFF" stopOpacity={end} />
      </linearGradient>,
      <rect
        key="rectangle"
        x={x}
        y={y}
        width={60}
        height={60}
        fill={`url(#gradient-${x}-${y})`}
      />
    ]
  );
};

const LoadingSpinner = () => {
  return (
    <svg>
      {/* outer circle */}
      <OpticalGradient definitions={definitions} />
      {/* arc */}
    </svg>
  );
};

great success, gradient implemented

Success! We let optical gradient data on their own in an array, outside of our render method (they could come from somewhere else, who knows); and we also created a React component only made of SVG elements! 🙌

Here we are rendering arrays directly, this only work with React 16: prior to this version, you would have a to wrap two or more adjacent JSX elements in one element. React 16 is awesome, and not just because you can render arrays. I highly recommend checking out the official React blog, as well as the docs: both are gold mines of information and pleasant to read! 🤓

By the way, there is no z-index concept in SVG: the <OpticalGradient /> element position is therefore important! It is placed between the outer circle and the arc, so it’s over the outer circle & below the arc.

One CSS Animation to rule them all

La cerise sur le gâteau. 🍰

Time for the final touch: let’s make that spinner spin!

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

These keyframes are a love declartion to our LoadingSpinner component, and then once tied to our SVG elements, they will enter in motion.

We’ll use inline-styles to set elements spin 1 turn in 1.6 in a linear way so they will be able to repeat infinitely free of bumps. The outer circle is actually static, it’s the opacity effect driven by the rectangles that spins. The arc is animated in reverse direction so we get this pretty cool crossing-without-touching effect:

const animation = 'spin 1.6s linear infinite';

const OpticalGradient = props => {
  return props.definitions.map(
    ({ x, y, start, end }) => [
      /* linear gradient */
      <rect
        key="rectangle"
        x={x}
        y={y}
        width={60}
        height={60}
        fill={`url(#gradient-${x}-${y})`}
        // the style prop is an object
        style={{ animation }}
      />
    ]
  );
};

const LoadingSpinner = () => {
  return (
    <svg viewBox="0 0 120 120" fill="transparent">
      {/*
       other elements
      */}
      <circle
        r={36}
        cx={60}
        cy={60}
        fill="transparent"
        stroke="url(#arc-gradient)"
        strokeWidth={2}
        // we re-use the animation defined above, 
        // make it turn in the opposite direction
    // and add a small delay to make it start before the opacity layer
        style={{ animation: `${animation} reverse -0.2s`}}
      />
    </svg>
  );
};

And here we are:

wicked animation, not really what we wanted

Holy cow! What’s happening here?! 😱

Hum… Well, the browser is actually doing exactly what we have told it to do: our spin animation is based on the transform CSS property. And with SVG elements, this property acts differently than with classic DOM elements. 😅 If you want to learn more about that, Ana Tudor wrote an excellent article on the subject, with comparisons between different properties, elements and browser!

To solve this (unexpected) issue, we are going to use the transform-origin CSS property:

  • the arc needs to spin around its center (the center of the SVG)
  • each rectangle needs to spin around the center of the arc (the center of the SVG)

Hopefully, we designed our SVG in a pretty simple way, a square of 120x120, so it’s straight-forward to get the absolute coordinate we want all element to spin around:

// abstract of the `style` prop
{
  animation,
  transformOrigin: '60px 60px',
}

2 circles & 4 rectangles later… The Awesome Spinning Loader!

THE AWESOME SPINNING LOADER YEAH

Aw yeah! 🌀 Ticking, spinning, resizing, loading, no matter what, this LoadingSpinner component definitely gets the ball rolling! 🎉

You can check the final result on this pen, which displays our LoadingSpinner alongside its original inspiration, (👏 to Dominic Nguyen for building it).

Let’s not forget that both components have their own set of trade-offs.

The SVG one has its styles encapsulated on its own, and could be even more if you use a CSS-in-JS, like styled-components 💅. But a full-fledged CSS solution may align with your current design system as it is for the original spinner.

The SVG one is also totally responsive, scalable vector graphics for the win; but may not render well on non-uniform backgrounds (the gradients stops use #fff in our example)!

In the end, I can only recommend to pick the solution that feels good, the one you like and fit with the scope of the project you are working on. I hope you’ve had a great time following / building this LoadingSpinner component, as I’ve had writing this post, my first tech tutorial.

If the SVG DOM was something obscure for you before, I also hope I’ve sparked your interest in experimenting and creating dope stuff with it! High five, let’s not be intimidated by the SVG DOM! 🖐


I’m Xavier Cazalot, @xav_cz on Twitter. I teach React (Native also) & GraphQL at OK GROW!, a consultancy based in Toronto, Canada.

Happy hacking! 🎩

We'll share what we've learned, get tips and info in your inbox occasionally