CSS
loaders and progress indicators are some of the most widely used
examples in tutorials and documentation. There are so many ways to
approach them. It’s possible that some approaches may be “better” than
others, but it also depends on what you want to accomplish. In this
article, Preethi demonstrates an approach using animated custom
properties, a conic gradient, CSS offset
, and emoji to create the illusion of a scooter racing along a donut track.
Let’s talk about progress indicators — or loaders. It’s true that there are so many tutorials about them and even more examples floating around CodePen. There was a time just a couple of years ago when loaders seemed to be the go-to example for framework documentation, next to to-do apps.
I
recently had the task of creating the loading state for a project, so
naturally, I looked to CodePen for inspiration. What I wanted was a
circular shape, and there is no shortage of examples. In many cases, the
approach is some combination of using the CSS border-radius
property to get a circular shape and @keyframes
to spin it from 0deg
to 360deg
.
I needed a little more than that. Specifically, I needed a donut shape that fills in the progress indicator as it goes from 0%
to 100%
.
Thankfully, I found great donut examples I could use for inspiration
and several different approaches. For example, I could use the “trick”
of an SVG with a stroke that animates with a combination of stroke-dasharray
and stroke-dashoffset
. Temani Afif has hundreds of examples that use a combination of CSS gradients and masks.
There was still more that I needed. What I really wanted was a donut progress indicator that not only fills in as the progress increases but sets a visual on it that moves with the progress. In other words, I wanted to make it look like an object is traveling around the donut, leaving a trail of progress behind.
See
that? The scooter has a circular track that fills in with a gradient as
it moves around the shape. If you’re using Firefox, you likely will
have trouble with the demo because it relies on a custom @property
that Firefox doesn’t support yet. However, it is supported in the Nightly version, so perhaps we have full support to look forward to soon.
In
the end, I wound up combining several of the techniques I found and
some additional considerations. I thought I would share the approach
because I like demonstrating how various ideas can come together to
create something different. This demo uses animated custom properties, a
conic gradient, CSS offset
, and emoji to produce the
effect. The truth is that you may find a different combination or set of
techniques that get the job done or fit your requirements better. This
is more of a thinking exercise.
Creating The Donut
Circles in CSS are fairly straightforward. We could draw one in SVG and forget CSS entirely. That’s a valid approach, but I’m comfortable working directly in CSS for this sort of thing. We start with a single element in the HTML:
From here, we set the circle’s dimensions. That can be done by declaring a width
and using an aspect-ratio
to maintain a perfect one-to-one shape.
Now we can round the shape with the border-radius
property:
That’s our shape! We won’t see anything yet, of course, because we haven’t filled it in with color. Let’s do that now with a conic-gradient
. We want one of those because the gradient moves in a circular direction by default, starting at 0%
and completing a full circle at 360deg
.
So far, so good:
What
we’re looking at is pretty much a pie chart, right? We’ve established a
circular shape and filled it in with a conical gradient that starts
with red and hits a hard color stop at #eee
, filling in the rest of the pie in a light gray.
The pie is delicious, but we’re aiming for a donut, and donuts have a hole cut out of the center. In the true spirit of CSS, there are different ways to approach this. Again, Temani has demonstrated time and again how CSS masks can do cut-outs. It’s a clean approach, too, because we can repurpose the same conical gradient to cut a circle from the center, only changing the color values to mask out the part we want to hide.
I
went a different route, partly for convenience and partly for the sake
of demonstrating how CSS is capable of approaching challenges in
multiple ways. So, you may even find yourself going with a different
route than what we’re demonstrating here. My approach is to use the ::before
pseudo-element of the .progress-circle
.
We lay it on top of the conical gradient with absolute positioning,
fill it with a solid color, and size it so it eclipses part of the main
shape. It’s basically a smaller solid-colored circle on top of a larger
gradient-filled circle.
Notice what we’re doing to position the smaller circle. Since we’re working with ::before
, we need the CSS content
property to make it display, even with an empty value. From there,
we’re using absolute positioning, setting the smaller circle towards the
center with an inset
applied in all directions. We’re able to inherit
the larger circle’s border-radius
before setting a solid background color. We can’t forget to set
relative positioning on the larger circle to (a) set a stacking context
and (b) keep the smaller circle within the larger circle’s bounds.
That’s it for the donut! We accomplished it purely in CSS, relying on a combination of the border-radius
property, a conic-gradient
, and a well-positioned ::before
pseudo-elmement.
Animating The Progress
Have you worked with custom CSS properties? I’m not simply referring to defining --some-variable
with a value, but using @property
to register a property with a custom syntax. It’s magic how it allows
us to interpolate between values that we are normally unable to, such as color and angle values in gradients.
When we register a CSS custom property, we have to mention what its type is, for instance, whether the value is a <length>
, <number>
, <color>
or any of the 11 other types that are supported
at the time I’m writing this. This way, the browser understands what
sort of value it is working with, and when the time arises, it can
update the variable’s value for an animation.
I’m going to register a custom property called --p
, which is short for its syntax, <percentage>
, with an initial value of 10%
that will be the “starting” point for the progress indicator.
Now, we can use the --p
variable where we need it, such as where the hard color stops between red
and #eee
in the larger circle’s conical gradient that we’re using as the starting point.
We want to transition from the custom property’s initial value, 10%
, to a larger percentage in order to move the gradient’s hard color stop around the shape. So, let’s set up a CSS transition
that is designed to update the value of --p
.
We’re going to update the value on hover, transitioning from 10%
to 80%
:
One more small adjustment: I like to update the cursor
on hover so that it’s clearer what sort of interaction the user is
dealing with. In this case, we’re working with something indicating
progress, so that’s how we’ll configure it:
Our circle is done! We can now hover over the element, and the conical gradient’s hard color stops transitions from 10%
to 80%
behind the smaller circle that is hiding the rest of the gradient to imply a donut shape. We registered a custom @property
with an initial value, applied it to the gradient, and updated the value on hover.
Moving Around The Circle
The final part of this exercise is to work on the progress indicator. We’re using the gradient to indicate progress, but I want the additional visual aid of an object that travels around the larger circle with the gradient as it transitions values.
The idea I had was a little scooter that appears to leave a gradient trail behind it. We already have the circle and the gradient, so all we need is the scooter and a way to make it use the larger circle as a track to drive around.
Let’s go ahead and add the scooter to the HTML as an emoji:
If
we had decided to create the initial donut shape with SVG, then we
could have used the same path we used for the larger circle as the
track. However, we can still get the same path-making powers in CSS
using the offset-path
property. It’s so much like writing SVG in CSS that we can actually use the exact same coordinates for an SVG circle in the path()
:
SVG path coordinates are difficult to read, but this is what we’re doing in this particular path:
M 100, 0
: This moves the position of the starting point on an X-Y coordinate system, where100
is along the X-axis and equal to the larger circle’s radius, or one-half of its width,200px
. The starting point is set to0
on the Y-axis, placing it at the top of the shape. So, we’re starting at the top-center of the larger circle.a 100 100
: This sets an arc with horizontal and vertical radii of100
, giving us a new circle. Even though we won’t technically see the circle, it is drawn in there, providing the scooter with an invisible track that follows the shape of the larger circle.
One more thing! We have a starting point for the scooter, thanks to the coordinates in the offset-path
. The CSS offset-distance
property lets us define the end point where we plan to offset the scooter, which is exactly equal to the --p
custom property.
We’re already updating our custom --p
property on hover to help move the conical gradient’s hard stop position from an initial value of 10%
to 80%
. We should do the same for the scooter so they move together.
I’m using the child combinator (>
)
since the indicator is a direct child of the circle. If your design
includes additional elements or requires the scooter to be a further
descendant, then you might consider a general descendant selector
instead.
The Final Result
Here’s
everything we covered in a single CSS snippet. I’ve cleaned things up a
tiny bit, such as setting up variables for recurring values, like the --size
of the circle.
A scooter and a solid gradient are only one idea. How about different objects with different trails?
I’ve been referring to this component as both a “progress indicator” and a “loader” throughout the article. There
is a difference between displaying progress and loading states, but
it’s also possible for a loading state to display the loading progress. That’s why I’m using a generic <div>
as a <figure>
in the example, but you could just as well use it on more semantic HTML elements, like <progress>
or <meter>
depending on your specific use case. For accessibility, you might
consider incorporating descriptive text that can be announced as
assistive-technology-friendly sentences that describe the data.
Let me know if you use this on a project and how you approach it. Share it with me in the comments, and we can compare notes.
No comments:
Post a Comment