How I Made a Generator for SVG Loaders With Sass and SMIL Options

Avatar of Mariana Beldi
Mariana Beldi on

UGURUS offers elite coaching and mentorship for agency owners looking to grow. Start with the free Agency Accelerator today.

While learning Vue.js, I started building free web tools that involved the exploration of SVG, with the goal of learning something about both! Let’s take a look at one of those tools: a generator that makes SVG loaders and lets you choose between SMIL or Sass animation, different styles, colors, shapes, and effects. It even lets you paste in a custom path or text, and then download the final SVG, copy the code, or open a demo over at CodePen.

How it started

Three coincidences led me to build a generator for SVG loaders.

Coincidence 1: Sarah Drasner’s book

The first time I read about Sass loops was in Sarah Drasner’s SVG Animations. She shows how to stagger animations with a Sass function (like the does in Chapter 6, “Animating Data Visualizations”).

I was inspired by that chapter and the possibilities of Sass loops.

Coincidence 2: A GIF

At that same point in life, I was asked to replicate a “loader” element, similar to Apple’s old classic.

A round segmented spinner where each segment fades in and out in succession to create a circling effect.
This is a mockup of the loader I was asked to make.

I referenced Sarah’s example to make it happen. This is the Sass loop code I landed on:

@for $i from 1 to 12 {
  .loader:nth-of-type(#{$i}) {
    animation: 1s $i * 0.08s opacityLoader infinite;
  }
}
@keyframes opacityLoader {
 to { opacity: 0; }
}

This defines a variable for a number (i) from 1 to 12 that increases the delay of the animation with every :nth-child element. It was the perfect use case to animate as many elements as I wanted with only two lines of Sass, saving me CSS declarations for each of the delays I needed. This is the same animation, but written in vanilla CSS to show the difference:

.loader:nth-of-type(1) {
  animation: 1s 0.08s opacityLoader infinite;
}
.loader:nth-of-type(2) {
  animation: 1s 0.16s opacityLoader infinite;
}

/* ... */

.loader:nth-of-type(12) {
  animation: 1s 0.96s opacityLoader infinite;
}
@keyframes opacityLoader {
  to { opacity: 0; }
}

Coincidence 3: An idea

With these things going on in my head, I had an idea for a gallery of loaders, where each loader is made from the same Sass loop. I always struggle to find these kinds of things online, so I thought it might be useful for others, not to mention myself.

I had already built this kind of thing before as a personal project, so I ended up building a loader generator. Let me know if you find bugs in it!

One loader, two outputs

I was blocked by my own developer skills while creating a generator that produces the right Sass output. I decided to try another animation approach with SMIL animations, and that’s what I wound up deciding to use.

But then I received some help (thanks, ekrof!) and got Sass to work after all.

So, I ended up adding both options to the generator. I found it was a challenge to make both languages return the same result. In fact, they sometimes produce different results.

SMIL vs. CSS/Sass

I learned quite a bit about SMIL and CSS/Sass animations along the way. These are a few of the key takeaways that helped me on my way to making the generator:

  • SMIL doesn’t rely on any external resources. It animates SVG via presentation attributes directly in the SVG markup. That’s something that neither CSS nor Sass can do.
  • SMIL animations are preserved when an SVG is embedded as an image or as a background image. It is possible to add a CSS <style> block directly inside the SVG, but not so much with Sass, of course. That’s why there is an option to download the actual SVG file when selecting the SMIL option in the generator.
  • SMIL animations look a bit more fluid. I couldn’t find the reason for this (if anyone has any deeper information here, please share!). I though it was related to GPU acceleration, but it seems they both use the same animation engine.
Two spinners, one left and one right. They are both red and consist of circles that fade in and out in succession as an animated GIF.
SMIL (left) and Sass (right)

You might notice a difference in the chaining of the animations between both languages:

  • I used additive="sum" in SMIL to add animations one after the other. This makes sure each new animation effect avoids overriding the previous animation.
  • That said, in CSS/Sass, the W3C points out that [when] multiple animations are attempting to modify the same property, then the animation closest to the end of the list of names wins.

That’s why the order in which animations are applied might change the Sass output.

Working with transforms

Working with transformations in the loader’s styling was a big issue. I had applied transform: rotate inline to each shape because it’s a simple way to place them next to each other in a circle and with a face pointing toward the center.

<svg>
  <!-- etc. -->
  <use class="loader" xlink:href="#loader" transform="rotate(0 50 50)" />
  <use class="loader" xlink:href="#loader" transform="rotate(30 50 50)" />
  <use class="loader" xlink:href="#loader" transform="rotate(60 50 50)" />
  <!-- etc. -->
</svg>

I could declare a type in SMIL with <animateTransform> (e.g. scale or translate) to add that specific transform to the original transformation of each shape:

<animateTransform
  attributeName="transform"
  type="translate"
  additive="sum"
  dur="1s"
  :begin="`${i * 0.08}s`"
  repeatCount="indefinite"
  from="0 0"
  to="10"
/>

But instead, transform in CSS was overriding any previous transform applied to the inline SVG. In other words, the original position reset to 0 and showed a very different result from what SMIL produced. That meant the animations wound up looking identical no matter what.

The same two red spinners as before but with different results. The SMIL version on the left seems to work as expected but the Sass one on the right doesn't animate in a circle like it should.

The (not very pretty) solution to make the Sass similar to SMIL was to place each shape inside a group (<g>) element, and apply the inline rotation to the groups, and the animation to the shapes. This way, the inline transform isn’t affected by the animation.

<svg>
  <!-- etc. -->
  <g class="loader" transform="rotate(0 50 50)">
    <use xlink:href="#loader" />
  </g>
  <g class="loader" transform="rotate(30 50 50)">
    <use xlink:href="#loader" />
  </g>
  <!-- etc. -->
</svg>

Now both languages have a very similar result.

The technology I used

I used Vue.js and Nuxt.js. Both have great documentation, but there are more specific reasons why I choose them.

I like Vue for lots of reasons:

  • Vue encapsulates HTML, CSS, and JavaScript as a “single file component” where all the code lives in a single file that’s easier to work with.
  • The way Vue binds and dynamically updates HTML or SVG attributes is very intuitive.
  • HTML and SVG don’t require any extra transformations (like making the code JSX-compatible).

As far as Nuxt goes:

  • It has a quick boilerplate that helps you focus on development instead of configuration.
  • There’s automatic routing and it supports auto-importing components.
  • It’s a good project structure with pages, components, and layouts.
  • It’s easier to optimize for SEO, thanks to meta tags.

Let’s look at a few example loaders

What I like about the end result is that the generator isn’t a one-trick pony. There’s no one way to use it. Because it outputs both SMIL and CSS/Sass, there are several ways to integrate a loader into your own project.

Download the SMIL SVG and use it as a background image in CSS

Like I mentioned earlier, SMIL features are preserved when an SVG is used as a background image file. So, simply download the SVG from the generator, upload it to your server, and reference it in CSS as a background image.

Similarly, we could use the SVG as a background image of a pseudo-element:

Drop the SVG right into the HTML markup

The SVG doesn’t have to be a background image. It’s just code, after all. That means we can simply drop the code from the generator into our own markup and let SMIL do its thing.

Use a Sass loop on the inline SVG

This is what I was originally inspired to do, but ran into some roadblocks. Instead of writing CSS declarations for each animation, we can use the Sass loop produced by the generator. The loop targets a .loader class that’s already applied to the outputted SVG. So, once Sass is compiled to CSS, we get a nice spinning animation.

I’m still working on this

My favorite part of the generator is the custom shape option where you can add text, emojis, or any SVG element to the mix:

The same circle spinner but using custom SVG shapes: one a word, one a poop emoji, and bright pink and orange asterisk.
Custom text, emoji, and SVG

What I would like to do is add a third option for styles to have just one element where you get to work with your own SVG element. That way, there’s less to work with, while allowing for simpler outputs.

The challenge with this project is working with custom values for so many things, like duration, direction, distance, and degrees. Another challenge for me personally is becoming more familiar with Vue because I want to go back and clean up that messy code. That said, the project is open source, and pull requests are welcome! Feel free to send suggestions, feedback, or even Vue course recommendations, especially ones related to SVG or making generators.

This all started with a Sass loop that I read in a book. It isn’t the cleanest code in the world, but I’m left blown away by the power of SMIL animations. I highly recommend Sarah Soueidan’s guide for a deeper dive into what SMIL is capable of doing.

If you’re curious about the safety of SMIL, that is for good reason. There was a time when Chrome was going to entirely deprecated SMIL (see the opening note in MDN). But that deprecation has been suspended and hasn’t (seemingly) been talked about in a while.

Can I use SMIL?

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
54No796

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
11511536.0-6.1