It was a couple of years in the past through the 2020 Olympics in Tokyo 2020 that I made a demo of animated 3D Olympic rings. I prefer it, it appears to be like nice, and I like the impact of the rings crossing one another.
However the code itself is type of outdated. I wrote it in SCSS, and crookedly at that. I do know it might be higher, no less than by trendy requirements.
So, I made a decision to construct the demo once more from scratch in honor of this 12 months’s Olympics. I’m writing vanilla CSS this time, leveraging trendy options like trigonometric features for fewer magic numbers and the relative colour syntax for higher colour administration. The kicker, seems, is that the brand new demo winds up being extra environment friendly with fewer traces of code than the outdated SCSS model I wrote in 2020!
Take a look at the CSS tab in that first demo once more as a result of we’ll wind up with one thing vastly totally different — and higher — with the method we’re going to make use of collectively. So, let’s start!
The markup
We’ll use layers to create the 3D impact. These layers are positioned one after the opposite (on the z-axis) to get the depth of the 3D object which, in our case, is a hoop. The mix of the form, measurement, and colour of every layer — plus the way in which they range from layer to layer — is what creates the total 3D object.
On this case, I’m utilizing 16 layers the place every layer is a special shade (with the darker layers stacked on the again) to get a easy lighting impact, and utilizing the dimensions and thickness of every layer to ascertain a spherical, round form.
So far as HTML goes, we want 5 <div> parts, one for every ring, the place every <div> comprises 16 parts that act because the layers, which I’m wrapping in <i> tags. These 5 rings we’ll put in a dad or mum container to carry issues collectively. We’ll give the dad or mum container a .rings class and every ring, creatively, a .ring class.
That is an abbreviated model of the HTML displaying how that comes collectively:
<div class=”rings”>
<div class=”ring”>
<i model=”–i: 1;”></i>
<i model=”–i: 2;”></i>
<i model=”–i: 3;”></i>
<i model=”–i: 4;”></i>
<i model=”–i: 5;”></i>
<i model=”–i: 6;”></i>
<i model=”–i: 7;”></i>
<i model=”–i: 8;”></i>
<i model=”–i: 9;”></i>
<i model=”–i: 10;”></i>
<i model=”–i: 11;”></i>
<i model=”–i: 12;”></i>
<i model=”–i: 13;”></i>
<i model=”–i: 14;”></i>
<i model=”–i: 15;”></i>
<i model=”–i: 16;”></i>
</div>
<!– 4 extra rings… –>
</div>
Notice the –i customized property I’ve dropped on the model attribute of every <i> component:
<i model=”–i: 1;”></i>
<i model=”–i: 2;”></i>
<i model=”–i: 3;”></i>
<!– and so on. –>
We’re going to make use of –i to calculate every layer’s place, measurement, and colour. That’s why I’ve set their values as integers in ascending order — these might be multipliers for arranging and styling every layer individually.
Professional tip: You possibly can keep away from writing the HTML for each layer by hand for those who’re engaged on an IDE that helps Emmet. But when not, no worries, as a result of CodePen does! Enter the next into your HTML editor then press the Tab key in your keyboard to increase it into 16 layers: i*16[style=”–i: $;”]
The (vanilla) CSS
Let’s begin with the dad or mum .rings container for now will simply get a relative place. With out relative positioning, the rings can be faraway from the doc move and wind up off the web page someplace when setting absolute positioning on them.
.rings {
place: relative;
}
.ring {
place: absolute;
}
Let’s do the identical with the <i> parts, however use CSS nesting to maintain the code compact. We’ll throw in border-radius whereas we’re at it to clip the boxy edges to type excellent circles.
.rings {
place: relative;
}
.ring {
place: absolute;
i {
place: absolute;
border-radius: 50%;
}
}
The final piece of primary styling we’ll apply earlier than transferring on is a customized property for the –ringColor. This’ll make coloring the rings pretty simple as a result of we will write it as soon as, after which override it on a layer-by-layer foundation. We’re declaring –ringColor on the border property as a result of we solely need coloration on the outer edges of every layer fairly than filling them in utterly with background-color:
.rings {
place: relative;
}
.ring {
place: absolute;
–ringColor: #0085c7;
i {
place: absolute;
inset: -100px;
border: 16px var(–ringColor) strong;
border-radius: 50%;
}
}
Did you discover I snuck one thing else in there? That’s proper, the inset property can also be there and set to a detrimental worth of 100px. That may look a bit of unusual, so let’s discuss that first as we proceed styling our work.
Damaging insetting
Setting a detrimental worth on the inset property signifies that the layer’s place falls exterior the .ring component. So, we would consider it extra like an “outset” as an alternative. In our case, the .ring has no measurement as there aren’t any content material or CSS properties to present it dimensions. Meaning the layer’s inset (or fairly “outset”) is 100px in every course, leading to a .ring that’s 200×200 pixels.
Let’s test in with what now we have up to now:
Positioning for depth
We’re utilizing the layers to create the impression of depth. We do this by positioning every of the 16 layers alongside the z-axis, which stacks parts from entrance to again. We’ll house every one a mere 2px aside — that’s all of the house we have to create a slight visible separation between every layer, giving us the depth we’re after.
Bear in mind the –i customized property we used within the HTML?
<i model=”–i: 1;”></i>
<i model=”–i: 2;”></i>
<i model=”–i: 3;”></i>
<!– and so on. –>
Once more, these are multipliers to assist us translate every layer alongside the z-axis. Let’s create a brand new customized property that defines the equation so we will apply it to every layer:
i {
–translateZ: calc(var(–i) * 2px);
}
What will we apply it to? We will use the CSS remodel property. This manner, we will rotate the layers vertically (i.e., rotateY()) whereas translating them alongside the z-axis:
i {
–translateZ: calc(var(–i) * 2px);
remodel: rotateY(-45deg) translateZ(var(–translateZ));
}
Shade for shading
For colour shading, we’ll darken the layers in keeping with their place in order that the layers get darker as we transfer from the entrance of the z-axis to the again. There are a couple of methods to do it. One is dropping in one other black layer with reducing opacity. One other is modifying the “lightness” channel in a hsl() colour operate the place the worth is “lighter” up entrance and incrementally darker in the direction of the again. A 3rd choice is enjoying with the layer’s opacity, however that will get messy.
Though now we have these three approaches, I believe the trendy CSS relative colour syntax is one of the best ways to go. We’ve already outlined a default –ringColor customized property. We will put it by way of the relative colour syntax to govern it into different colours for every ring <i> layer.
First, we want a brand new customized property we will use to calculate a “mild” worth:
.ring {
–ringColor: #0085c7;
i {
–light: calc(var(–i) / 16);
border: 16px var(–ringColor) strong;
}
}
We’ll use the calc()-ulated lead to one other customized property that places our default –ringColor by way of the relative colour syntax the place the –light customized property helps modify the ensuing colour’s lightness.
.ring {
–ringColor: #0085c7;
i {
–light: calc(var(–i) / 16);
–layerColor: rgb(from var(–ringColor) calc(r * var(–light)) calc(g * var(–light)) calc(b * var(–light)));
border: 16px var(–ringColor) strong;
}
}
That’s fairly an equation! However it solely appears to be like complicated as a result of the relative colour syntax wants arguments for every channel within the colour (RGB) and we’re calculating every one.
rgb(from origin-color channelR channelG channelB)
So far as the calculations go, we multiply every RGB channel by the –light customized property, which is a quantity between 0 and 1 divided by the variety of layers, 16.
Time for one more test to see the place we’re at:
Creating the form
To get the round ring form, we’ll set the layer’s measurement (i.e., thickness) with the border property. That is the place we will begin utilizing trigonometry in our work!
We would like the thickness of every ring to be a worth between 0deg to 180deg — since we’re solely truly making half of a circle — so we are going to divide 180deg by the variety of layers, 16, which comes out to 11.25deg. Utilizing the sin() trigonometric operate (which is equal to the reverse and hypotenuse sides of a proper angle), we get this expression for the layer’s –size:
–size: calc(sin(var(–i) * 11.25deg) * 16px);
So, no matter –i is within the HTML, it acts as a multiplier for calculating the layer’s border thickness. We’ve been declaring the layer’s border like this:
i {
border: 16px var(–ringColor) strong;
)
Now we will substitute the hard-coded 16px worth with –size calculation:
i {
–size: calc(sin(var(–i) * 11.25deg) * 16px);
border: var(–size) var(–layerColor) strong;
)
However! As you could have observed, we aren’t altering the layer’s measurement after we change its border width. Because of this, the spherical profile solely seems on the layer’s inside aspect. The important thing factor right here is knowing that setting the –size with the inset property which suggests it doesn’t have an effect on the component’s box-sizing. The result’s a 3D ring for positive, however many of the shading is buried.
⚠️ Auto-playing media
We will convey the shading out by calculating a brand new inset for every layer. That’s type of what I did within the 2020 model, however I believe I’ve discovered a neater means: add an overview with the identical border values to finish the arc on the outer aspect of the ring.
i {
–size: calc(sin(var(–i) * 11.25deg) * 16px);
border: var(–size) var(–layerColor) strong;
define: var(–size) var(–layerColor) strong;
}
We’ve a extra natural-looking ring now that we’ve established an overview:
Animating the rings
I needed to animate the ring in that final demo to check the ring’s shading earlier than and after. We’ll use that very same animation within the ultimate demo, so let’s break down how I did that earlier than we add the opposite 4 rings to the HTML
I’m not attempting to do something fancy; I’m simply setting the rotation on the y-axis from -45deg to 45deg (the translateZ worth stays fixed).
@keyframes ring {
from { remodel: rotateY(-45deg) translateZ(var(–translateZ, 0)); }
to { remodel: rotateY(45deg) translateZ(var(–translateZ, 0)); }
}
As for the animation property, I’ve given named it ring , and a hard-coded (no less than for now) a period of 3s, that loops infinitely. Setting the animation’s timing operate with ease-in-out and alternate, respectively, offers us a easy back-and-forth movement.
i {
animation: ring 3s infinite ease-in-out alternate;
}
That’s how the animation works!
Including extra rings
Now we will add the remaining 4 rings to the HTML. Bear in mind, now we have 5 rings whole and every ring comprises 16 <i> layers. It might look so simple as this:
<div class=”rings”>
<div class=”ring”> <!– <i> layers –> </div>
<div class=”ring”> <!– <i> layers –> </div>
<div class=”ring”> <!– <i> layers –> </div>
<div class=”ring”> <!– <i> layers –> </div>
<div class=”ring”> <!– <i> layers –> </div>
</div>
There’s one thing elegant in regards to the simplicity of this markup. And we might use the CSS nth-child() pseudo-selector to pick them individually. I like being a bit extra declarative than that and am going to present every .ring and extra class we will use to explicitly choose a given ring.
<div class=”rings”>
<div class=”ring ring__1″> <!– layers –> </div>
<div class=”ring ring__2″> <!– layers –> </div>
<div class=”ring ring__3″> <!– layers –> </div>
<div class=”ring ring__4″> <!– layers –> </div>
<div class=”ring ring__5″> <!– layers –> </div>
</div>
Our activity now could be to regulate every ring individually. Proper now, every thing appears to be like like the primary ring we made collectively. We’ll use the distinctive courses we simply set within the HTML to present them their very own colour, place, and animation period.
The excellent news? We’ve been utilizing customized properties this complete time! All now we have to do is replace the values in every ring’s distinctive class.
.ring {
&.ring__1 { –ringColor: #0081c8; –duration: 3.2s; –translate: -240px, -40px; }
&.ring__2 { –ringColor: #fcb131; –duration: 2.6s; –translate: -120px, 40px; }
&.ring__3 { –ringColor: #444444; –duration: 3.0s; –translate: 0, -40px; }
&.ring__4 { –ringColor: #00a651; –duration: 3.4s; –translate: 120px, 40px; }
&.ring__5 { –ringColor: #ee334e; –duration: 2.8s; –translate: 240px, -40px; }
}
Should you’re questioning the place these –ringColor values got here from, I primarily based them on the Worldwide Olympic Committee’s documented colours. Every –duration is barely offset from each other to stagger the motion between rings, and the rings are –translate‘d 120px aside after which staggered vertically by alternating their place 40px and -40px.
Let’s apply the interpretation stuff to the .ring parts:
.ring {
remodel: translate(var(–translate));
}
Earlier, we set the animation’s period to a hard-coded three seconds:
i {
animation: ring 3s infinite ease-in-out alternate;
}
That is the time to interchange that with a customized property that calculates the period for every ring individually.
i {
animation: ring var(–duration) -10s infinite ease-in-out alternate;
}
Whoa, whoa! What’s the -10s worth doing in there? Though every ring layer is ready to animate for a special period, the beginning angle of the animations is all the identical. Including a continuing detrimental delay on altering durations will guarantee that every ring’s animation begins at a special angle.
Now now we have one thing that’s nearly completed:
Some ultimate touches
We’re on the ultimate stretch! The animation appears to be like fairly nice as-is, however I wish to add two extra issues. The primary one is a small-10deg “tilt” on the x-axis of the dad or mum .rings container. This can make it seem like we’re viewing issues from the next perspective.
.rings {
rotate: x -10deg;
}
The second crowning glory has to do with shadows. We will actually punctuate the 3D depth of our work and all it takes is choosing the .ring component’s ::after pseudo-element and styling it like a shadow.
First, we’ll set the width of the pseudos’ border and description to a continuing (24px) whereas setting the colour to a semi-transparent black (#0003). Then we’ll translate them so they look like additional away. We’ll additionally inset them in order that they line up with the precise rings. Principally, we’re shifting the pseudo-elements round relative to the precise component.
.ring {
/* and so on. */
&::after {
content material: ”;
place: absolute;
inset: -100px;
border: 24px #0003 strong;
define: 24px #0003 strong;
translate: 0 -100px -400px;
}
}
The pseudos don’t look very shadow-y in the intervening time. However they’ll if we blur() them a bit:
.ring {
/* and so on. */
&::after {
content material: ”;
place: absolute;
inset: -100px;
border: 24px #0003 strong;
define: 24px #0003 strong;
translate: 0 -100px -400px;
filter: blur(12px);
}
}
The shadows are additionally fairly box-y. Let’s make sure that they’re spherical just like the rings:
.ring {
/* and so on. */
&::after {
content material: ”;
place: absolute;
inset: -100px;
border: 24px #0003 strong;
define: 24px #0003 strong;
translate: 0 -100px -400px;
filter: blur(12px);
border-radius: 50%;
}
}
Oh, and we should set the identical animation on the pseudo in order that the shadows transfer in concord with the rings:
.ring {
/* and so on. */
&::after {
content material: ”;
place: absolute;
inset: -100px;
border: 24px #0003 strong;
define: 24px #0003 strong;
translate: 0 -100px -400px;
filter: blur(12px);
border-radius: 50%;
animation: ring var(–duration) -10s infinite ease-in-out alternate;
}
}
Closing demo
Let’s cease and admire our accomplished work:
On the finish of the day, I’m actually proud of the 2024 model of the Olympic rings. The 2020 model obtained the job completed and was most likely the proper method for that point. However with all the options we’re getting in trendy CSS at the moment, I had loads of alternatives to enhance the code in order that it’s not solely extra environment friendly however extra reusable — for instance, this might be utilized in one other mission and “themed” just by updating the –ringColor customized property.
Finally, this train proved to me the ability and adaptability of recent CSS. We took an present concept with complexities and recreated it with simplicity and magnificence.
CSS Olympic Rings initially printed on CSS-Tips, which is a part of the DigitalOcean household. It’s best to get the publication.
Subscribe to MarketingSolution.
Receive web development discounts & web design tutorials.
Now! Lets GROW Together!