Wednesday, August 7, 2024
HomeProgrammingCSS Olympic Rings | CSS-Methods

CSS Olympic Rings | CSS-Methods


It was a number of years in the past throughout the 2020 Olympics in Tokyo 2020 that I made a demo of animated 3D Olympic rings. I prefer it, it seems nice, and I really like the impact of the rings crossing one another.

However the code itself is type of previous. I wrote it in SCSS, and crookedly at that. I do know it may very well be higher, not less than by fashionable 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 fashionable options like trigonometric capabilities for fewer magic numbers and the relative shade syntax for higher shade administration. The kicker, seems, is that the brand new demo winds up being extra environment friendly with fewer traces of code than the previous SCSS model I wrote in 2020!

Have a look at the CSS tab in that first demo once more as a result of we’ll wind up with one thing vastly completely different — and higher — with the strategy 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 mixture of the form, measurement, and shade of every layer — plus the way in which they differ 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 unique shade (with the darker layers stacked on the again) to get a easy lighting impact, and utilizing the scale and thickness of every layer to determine a spherical, round form.

So far as HTML goes, we’d like 5 <div> components, one for every ring, the place every <div> accommodates 16 components 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 exhibiting 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>

Observe the --i customized property I’ve dropped on the model attribute of every <i> factor:

<i model="--i: 1;"></i>
<i model="--i: 2;"></i>
<i model="--i: 3;"></i>
<!-- and so forth. -->

We’re going to make use of --i to calculate every layer’s place, measurement, and shade. That’s why I’ve set their values as integers in ascending order — these will likely be multipliers for arranging and styling every layer individually.

Professional tip: You may keep away from writing the HTML for every layer by hand if you happen to’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 circulate 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> components, 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 kind good circles.

.rings {
  place: relative;
}

.ring {
  place: absolute;
  
  i {
    place: absolute;
    border-radius: 50%;
  }
}

The final piece of fundamental styling we’ll apply earlier than shifting on is a customized property for the --ringColor. This’ll make coloring the rings pretty simple as a result of we are able to 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 relatively than filling them in fully with background-color:

.rings {
  place: relative;
}

.ring {
  place: absolute;
  --ringColor: #0085c7;
  
  i {
    place: absolute;
    inset: -100px;
    border: 16px var(--ringColor) stable;
    border-radius: 50%;
  }
}

Did you discover I snuck one thing else in there? That’s proper, the inset property can be there and set to a adverse worth of 100px. Which may look slightly unusual, so let’s speak about that first as we proceed styling our work.

Adverse insetting

Setting a adverse worth on the inset property implies that the layer’s place falls exterior the .ring factor. So, we would consider it extra like an “outset” as a substitute. In our case, the .ring has no measurement as there are not any content material or CSS properties to provide it dimensions. Meaning the layer’s inset (or relatively “outset”) is 100px in every route, leading to a .ring that’s 200×200 pixels.

A blue-bordered transparent square drawn on top of graph lines with arrows inside indicating the ring layer offsets and how they affect the size of the ring element.

Let’s examine in with what we have now to date:

Positioning for depth

We’re utilizing the layers to create the impression of depth. We try this by positioning every of the 16 layers alongside the z-axis, which stacks components from entrance to again. We’ll area every one a mere 2px aside — that’s all of the area 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 forth. -->

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 are able to apply it to every layer:

i {
  --translateZ: calc(var(--i) * 2px);
}

What can we apply it to? We will use the CSS rework property. This manner, we are able to rotate the layers vertically (i.e., rotateY()) whereas translating them alongside the z-axis:

i {
  --translateZ: calc(var(--i) * 2px);

  rework: rotateY(-45deg) translateZ(var(--translateZ));
}

Coloration for shading

For shade 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 number of methods to do it. One is dropping in one other black layer with lowering opacity. One other is modifying the “lightness” channel in a hsl() shade perform the place the worth is “lighter” up entrance and incrementally darker in direction of the again. A 3rd possibility is taking part in with the layer’s opacity, however that will get messy.

Although we have now these three approaches, I feel the trendy CSS relative shade syntax is one of the best ways to go. We’ve already outlined a default --ringColor customized property. We will put it by means of the relative shade syntax to govern it into different colours for every ring <i> layer.

First, we’d like a brand new customized property we are able to use to calculate a “gentle” worth:

.ring {
  --ringColor: #0085c7;
  
  i {
    --light: calc(var(--i) / 16);

    border: 16px var(--ringColor) stable;
  }
}

We’ll use the calc()-ulated lead to one other customized property that places our default --ringColor by means of the relative shade syntax the place the --light customized property helps modify the ensuing shade’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) stable;
  }
}

That’s fairly an equation! Nevertheless it solely seems complicated as a result of the relative shade syntax wants arguments for every channel within the shade (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 an additional examine 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 are able to begin utilizing trigonometry in our work!

We wish the thickness of every ring to be a price between 0deg to 180deg — since we’re solely really making half of a circle — so we’ll divide 180deg by the variety of layers, 16, which comes out to 11.25deg. Utilizing the sin() trigonometric perform (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 now have been declaring the layer’s border like this:

i {
  border: 16px var(--ringColor) stable;
)

Now we are able to change the hard-coded 16px worth with --size calculation:

i {
  --size: calc(sin(var(--i) * 11.25deg) * 16px);

  border: var(--size) var(--layerColor) stable;
)

However! As you could have observed, we aren’t altering the layer’s measurement after we change its border width. In consequence, the spherical profile solely seems on the layer’s interior facet. The important thing factor right here is knowing that setting the --size with the inset property which implies it doesn’t have an effect on the factor’s box-sizing. The result’s a 3D ring for certain, 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 feel I’ve discovered a better approach: add an define with the identical border values to finish the arc on the outer facet of the ring.

i {
  --size: calc(sin(var(--i) * 11.25deg) * 16px);

  border: var(--size) var(--layerColor) stable;
  define: var(--size) var(--layerColor) stable;
}

We now have a extra natural-looking ring now that we’ve established an define:

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 closing demo, so let’s break down how I did that earlier than we add the opposite 4 rings to the HTML

I’m not making an attempt 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 { rework: rotateY(-45deg) translateZ(var(--translateZ, 0)); }
  to { rework: rotateY(45deg) translateZ(var(--translateZ, 0)); }
}

As for the animation property, I’ve given named it ring , and a hard-coded (not less than for now) a period of 3s, that loops infinitely. Setting the animation’s timing perform with ease-in-out and alternate, respectively, offers us a clean back-and-forth movement.

i {
  animation: ring 3s infinite ease-in-out alternate;
}

That’s how the animation works!

Including extra rings

Now we are able to add the remaining 4 rings to the HTML. Bear in mind, we have now 5 rings complete and every ring accommodates 16 <i> layers. It may 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 concerning the simplicity of this markup. And we may use the CSS nth-child() pseudo-selector to pick them individually. I like being a bit extra declarative than that and am going to provide every .ring and extra class we are able to 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’s to regulate every ring individually. Proper now, every part seems like the primary ring we made collectively. We’ll use the distinctive courses we simply set within the HTML to provide them their very own shade, place, and animation period.

The excellent news? We’ve been utilizing customized properties this whole time! All we have now 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; }
}

For those who’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 components:

.ring {
  rework: 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 switch 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? Although every ring layer is about to animate for a unique period, the beginning angle of the animations is all the identical. Including a continuing adverse delay on altering durations will be sure that every ring’s animation begins at a unique angle.

Now we have now one thing that’s virtually completed:

Some closing touches

We’re on the closing stretch! The animation seems fairly nice as-is, however I need to add two extra issues. The primary one is a small-10deg “tilt” on the x-axis of the dad or mum .rings container. It will make it appear to be we’re viewing issues from a better perspective.

.rings {
  rotate: x -10deg;
}

The second of entirety has to do with shadows. We will actually punctuate the 3D depth of our work and all it takes is choosing the .ring factor’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. Mainly, we’re shifting the pseudo-elements round relative to the precise factor.

.ring {
  /* and so forth. */

  &::after {
    content material: '';
    place: absolute;
    inset: -100px;
    border: 24px #0003 stable;
    define: 24px #0003 stable;
    translate: 0 -100px -400px;
  }
}

The pseudos don’t look very shadow-y in the intervening time. However they are going to if we blur() them a bit:

.ring {
  /* and so forth. */

  &::after {
    content material: '';
    place: absolute;
    inset: -100px;
    border: 24px #0003 stable;
    define: 24px #0003 stable;
    translate: 0 -100px -400px;
    filter: blur(12px);
  }
}

The shadows are additionally fairly box-y. Let’s ensure that they’re spherical just like the rings:

.ring {
  /* and so forth. */

  &::after {
    content material: '';
    place: absolute;
    inset: -100px;
    border: 24px #0003 stable;
    define: 24px #0003 stable;
    translate: 0 -100px -400px;
    filter: blur(12px);
    border-radius: 50%;
  }
}

Oh, and we must set the identical animation on the pseudo in order that the shadows transfer in concord with the rings:

.ring {
  /* and so forth. */

  &::after {
    content material: '';
    place: absolute;
    inset: -100px;
    border: 24px #0003 stable;
    define: 24px #0003 stable;
    translate: 0 -100px -400px;
    filter: blur(12px);
    border-radius: 50%;
    animation: ring var(--duration) -10s infinite ease-in-out alternate;
  }
}

Remaining 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 bought the job finished and was in all probability the correct strategy for that point. However with all the options we’re getting in fashionable CSS right now, 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 may very well be utilized in one other venture and “themed” just by updating the --ringColor customized property.

Finally, this train proved to me the facility and suppleness of recent CSS. We took an present concept with complexities and recreated it with simplicity and class.

Previous articleiOS Prepared | Kodeco
RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments