A Donut Loader With a Traveling Emoji
CSS loaders and progress indicators are everywhere, and there are as many ways to build them as there are examples floating around places like CodePen. A standard approach might use border-radius to make a circle and @keyframes to spin it, but that only gets you so far. What if you need something that not only conveys progress as it fills up but also gives the impression of an object moving along the track, leaving a trail behind it?
The effect can be assembled from a few distinct CSS techniques: a custom property registered with @property, a conic-gradient for the fill, the offset-path property for motion, and an emoji to serve as the moving object. The result is a donut-shaped indicator that transitions from 10% to 80% on hover, with the chosen emoji zipping along the path in sync.
See the Pen [Circular animation with offset Pt. 1 [forked]](https://codepen.io/smashingmag/pen/vYvrwXo) by Preethi Sam.
Building the donut requires two concentric circles. The outer shape comes first, using a single element and its dimensions for the main track.
<div class="progress-circle"></div>
Set the size and round the corners:
.progress-circle {
width: 200px;
aspect-ratio: 1;
}
.progress-circle {
width: 200px;
aspect-ratio: 1;
border-radius: 50%;
}
Then fill the shape with a conical gradient. This gives a natural circular progression of color, useful for showing data like a pie chart.
.progress-circle {
width: 200px;
aspect-ratio: 1;
border-radius: 50%;
background: conic-gradient(red 10%, #eee 0);
}
See the Pen [Conic Gradient Circle [forked]](https://codepen.io/smashingmag/pen/zYyaera) by Geoff Graham.
To achieve the donut look, the ::before pseudo-element is positioned on top. It is a smaller circle, centered by inherit the border-radius and setting a solid background color to mask the center of the gradient.
.progress-circle {
/* previous styles */
position: relative;
}
.progress-circle::before {
content: '';
position: absolute;
inset: 20px;
border-radius: inherit;
background: white;
}
Relative positioning on the main element sets the stacking context, and absolute positioning keeps the smaller circle in place inside.
See the Pen [conic-gradient() [forked]](https://codepen.io/smashingmag/pen/GRPGzjg) by Preethi Sam.
Animating the Progress
To make the gradient progress smoothly, you need to be able to interpolate a value that directly controls the color stop. Registering a custom property with @property tells the browser the type of value it holds, such as a <percentage>, so it can animate it.
Define the custom property with an initial value:
@property --p {
syntax: '';
inherits: false;
initial-value: 10%;
}
This variable can then be used wherever the color stops are set in the gradient:
.progress-circle {
/* previous styles */
background: conic-gradient(red var(--p), #eee 0);
}
A transition is set to change this property’s value over time. Updating it on hover moves the hard color stop from the initial 10% to a larger percentage.
.progress-circle {
/* previous styles */
background: conic-gradient(red var(--p), #eee 0);
transition: --p 2s linear;
}
.progress-circle:hover{
--p: 80%;
}
The cursor is changed to make the interaction more obvious:
.progress-circle {
/* previous styles */
cursor: progress;
}
Now hovering over the element slides the gradient’s color stop from 10% to 80%, even though the center hole remains hidden by the ::before pseudo-element.
See the Pen [conic-gradient() animation [forked]](https://codepen.io/smashingmag/pen/mdaKvBe) by Preethi Sam.
The Object's Motion Path
To have a scooter emoji drive around the progress circle, CSS offers the offset-path property. This works much like an SVG path. The track is defined with an arc, and a starting point is declared. The path coordinates set a starting position at the top center, then an arc command with a radius that matches the main shape.
Add the emoji to the HTML:
<div class="progress-circle">
<div class="progress-indicator">🛵</div>
</div>
The path is set to follow the shape of the indicator:
.chart-indicator {
/* previous styles */
offset: path("M 100, 0 a 100 100 0 1 1 -.1 0 z");
}
The travel distance along the path is set with the offset-distance property. To have the scooter stay in sync with the progress bar, this distance is tied to the same custom property used for the gradient.
.chart-indicator {
/* previous styles */
offset-path: path("M 100, 0 a 100 100 0 1 1 -.1 0 z");
offset-distance: var(--p);
}
The scooter’s position updates along with the progress on hover:
.progress-circle:hover > .progress-indicator { --p: 80%; }
Note the direct child selector for the scooter. If the structure grows, a more general selector might be needed.
The Completed Component
Combining all the pieces, the code can be cleaned up by storing recurring values in variables.
/* Custom property */
@property --p {
syntax: '<percentage>';
inherits: false;
initial-value: 10%;
}
/* Large circle */
.progress-circle {
--size: 200px;
--p: 10%; /* fallback for no @property support */
background: conic-gradient(red calc(-60% + var(--p)), rgb(224, 187, 77) var(--p), #eee 0);
border-radius: 50%;
position: relative;
margin: auto;
cursor: progress;
}
/* Small circle pseudo-element */
.progress-circle::before {
content:'Going ten to eighty percent';
position: absolute;
inset: 20px;
text-align: center;
padding: 50px;
font: italic 9pt 'Enriqueta';
border-radius: inherit;
background: white;
}
/* The scooter track */
.progress-indicator {
--size: min-content;
offset: path("M 100,0 a 100 100 0 1 1 -.1 0 z");
offset-distance: var(--p);
font: 43pt serif;
transform: rotateY(180deg) translateX(-6px);
}
/* Update initial value on :hover */
.progress-circle:hover,
.progress-circle:hover > .progress-indicator {
--p: 80%;
}
/* Controls the width of larger circle and scooter track */
.progress-circle,
.progress-indicator {
width: var(--size);
transition: --p 2s linear;
}
See the Pen [Circular animation with offset Pt. 1 [forked]](https://codepen.io/smashingmag/pen/vYvrwXo) by Preethi Sam.
Different moving objects and their trails are possible. This showcases an idea; it’s more of a thinking exercise for how to combine techniques.
See the Pen [Circular animation with offset Pt. 2 [forked]](https://codepen.io/smashingmag/pen/gOZKJma) by Preethi Sam.
Whether you call it a loader or a progress indicator, consider the context for your usage. A generic <div> as a <figure> was sufficient here, but more semantic elements like <progress> or <meter> might be better for your use case. For better accessibility, include descriptive text that assistive technologies can announce. How you adapt this approach for your projects is up to you.



