Braydon Coyer not too long ago launched a month-to-month CSS artwork problem. He truly had reached out to me about donating a duplicate of my ebook Transfer Issues with CSS to make use of as a prize for the winner of the problem — which I used to be more than pleased to do!
The primary month’s problem? Spring. And when pondering of what to make for the problem, Slinkys instantly got here to thoughts. You already know Slinkys, proper? That basic toy you knock down the steps and it travels with its personal momentum.
A slinking Slinky
Can we create a Slinky strolling down stairs like that in CSS? That’s precisely the kind of problem I like, so I believed we may sort out that collectively on this article. Able to roll? (Pun supposed.)
Organising the Slinky HTML
Let’s make this versatile. (No pun supposed.) What I imply by that’s we would like to have the ability to management the Slinky’s habits by way of CSS customized properties, giving us the pliability of swapping values when we have to.
Right here’s how I’m setting the scene, written in Pug for brevity:
– const RING_COUNT = 10;
.container
.scene
.airplane(type=`–ring-count: ${RING_COUNT}`)
– let rings = 0;
whereas rings < RING_COUNT
.ring(type=`–index: ${rings};`)
– rings++;
These inline customized properties are a straightforward approach for us to replace the variety of rings and can turn out to be useful as we get deeper into this problem. The code above offers us 10 rings with HTML that appears one thing like this when compiled:
<div class=”container”>
<div class=”scene”>
<div class=”airplane” type=”–ring-count: 10″>
<div class=”ring” type=”–index: 0;”></div>
<div class=”ring” type=”–index: 1;”></div>
<div class=”ring” type=”–index: 2;”></div>
<div class=”ring” type=”–index: 3;”></div>
<div class=”ring” type=”–index: 4;”></div>
<div class=”ring” type=”–index: 5;”></div>
<div class=”ring” type=”–index: 6;”></div>
<div class=”ring” type=”–index: 7;”></div>
<div class=”ring” type=”–index: 8;”></div>
<div class=”ring” type=”–index: 9;”></div>
</div>
</div>
</div>
The preliminary Slinky CSS
We’re going to want some kinds! What we would like is a three-dimensional scene. I’m conscious of some issues we might wish to do later, in order that’s the pondering behind having an additional wrapper part with a .scene class.
Let’s begin by defining some properties for our “infini-slinky” scene:
:root {
–border-width: 1.2vmin;
–depth: 20vmin;
–stack-height: 6vmin;
–scene-size: 20vmin;
–ring-size: calc(var(–scene-size) * 0.6);
–plane: radial-gradient(rgb(0 0 0 / 0.1) 50%, clear 65%);
–ring-shadow: rgb(0 0 0 / 0.5);
–hue-one: 320;
–hue-two: 210;
–blur: 10px;
–speed: 1.2s;
–bg: #fafafa;
–ring-filter: brightness(1) drop-shadow(0 0 0 var(–accent));
}
These properties outline the traits of our Slinky and the scene. With nearly all of 3D CSS scenes, we’re going to set transform-style throughout the board:
* {
box-sizing: border-box;
transform-style: preserve-3d;
}
Now we want kinds for our .scene. The trick is to translate the .airplane so it seems to be like our CSS Slinky is shifting infinitely down a flight of stairs. I needed to mess around to get issues precisely the way in which I would like, so bear with the magic quantity for now, as they’ll make sense later.
.container {
/* Outline the scene’s dimensions */
top: var(–scene-size);
width: var(–scene-size);
/* Add depth to the scene */
remodel:
translate3d(0, 0, 100vmin)
rotateX(-24deg) rotateY(32deg)
rotateX(90deg)
translateZ(calc((var(–depth) + var(–stack-height)) * -1))
rotate(0deg);
}
.scene,
.airplane {
/* Guarantee our container take up the total .container */
top: 100%;
width: 100%;
place: relative;
}
.scene {
/* Shade is bigoted */
background: rgb(162 25 230 / 0.25);
}
.airplane {
/* Shade is bigoted */
background: rgb(25 161 230 / 0.25);
/* Overrides the earlier selector */
remodel: translateZ(var(–depth));
}
There’s a good bit occurring right here with the .container transformation. Particularly:
translate3d(0, 0, 100vmin): This brings the .container ahead and stops our 3D work from getting reduce off by the physique. We aren’t utilizing perspective at this degree, so we are able to get away with it.rotateX(-24deg) rotateY(32deg): This rotates the scene based mostly on our preferences.rotateX(90deg): This rotates the .container by 1 / 4 flip, which flattens the .scene and .airplane by default, In any other case, the 2 layers would seem like the highest and backside of a 3D dice.translate3d(0, 0, calc((var(–depth) + var(–stack-height)) * -1)): We are able to use this to maneuver the scene and middle it on the y-axis (effectively, truly the z-axis). That is within the eye of the designer. Right here, we’re utilizing the –depth and –stack-height to middle issues.rotate(0deg): Though, not in use in the intervening time, we might wish to rotate the scene or animate the rotation of the scene later.
To visualise what’s occurring with the .container, examine this demo and faucet anyplace to see the remodel utilized (sorry, Chromium solely. 😭):
We now have a styled scene! 💪
Styling the Slinky’s rings
That is the place these CSS customized properties are going to play their half. We now have the inlined properties –index and –ring-count from our HTML. We even have the predefined properties within the CSS that we noticed earlier on the :root.
The inline properties will play an element in positioning every ring:
.ring {
–origin-z:
calc(
var(–stack-height) – (var(–stack-height) / var(–ring-count))
* var(–index)
);
–hue: var(–hue-one);
–accent: hsl(var(–hue) 100% 55%);
top: var(–ring-size);
width: var(–ring-size);
border-radius: 50%;
border: var(–border-width) stable var(–accent);
place: absolute;
high: 50%;
left: 50%;
transform-origin: calc(100% + (var(–scene-size) * 0.2)) 50%;
remodel:
translate3d(-50%, -50%, var(–origin-z))
translateZ(0)
rotateY(0deg);
}
.ring:nth-of-type(odd) {
–hue: var(–hue-two);
}
Pay attention to how we’re calculating the –origin-z worth in addition to how we place every ring with the remodel property. That comes after positioning every ring with place: absolute .
Additionally it is price noting how we’re alternating the colour of every ring in that final ruleset. Once I first carried out this, I needed to create a rainbow slinky the place the rings went by way of the hues. However that provides a little bit of complexity to the impact.
Now we’ve received some rings on our raised .airplane:
Remodeling the Slinky rings
It’s time to get issues shifting! You could have seen that we set a transform-origin on every .ring like this:
.ring {
transform-origin: calc(100% + (var(–scene-size) * 0.2)) 50%;
}
That is based mostly on the .scene dimension. That 0.2 worth is half the remaining accessible dimension of the .scene after the .ring is positioned.
We may tidy this up a bit for certain!
:root {
–ring-percentage: 0.6;
–ring-size: calc(var(–scene-size) * var(–ring-percentage));
–ring-transform:
calc(
100%
+ (var(–scene-size) * ((1 – var(–ring-percentage)) * 0.5))
) 50%;
}
.ring {
transform-origin: var(–ring-transform);
}
Why that transform-origin? Properly, we want the ring to seem like is shifting off-center. Taking part in with the remodel of a person ring is an effective technique to work out the remodel we wish to apply. Transfer the slider on this demo to see the ring flip:
Add all of the rings again and we are able to flip the entire stack!
Hmm, however they aren’t falling to the following stair. How can we make every ring fall to the precise place?
Properly, we’ve a calculated –origin-z, so let’s calculate –destination-z so the depth adjustments because the rings remodel. If we’ve a hoop on high of the stack, it ought to wind up on the backside after it falls. We are able to use our customized properties to scope a vacation spot for every ring:
ring {
–destination-z: calc(
(
(var(–depth) + var(–origin-z))
– (var(–stack-height) – var(–origin-z))
) * -1
);
transform-origin: var(–ring-transform);
remodel:
translate3d(-50%, -50%, var(–origin-z))
translateZ(calc(var(–destination-z) * var(–flipped, 0)))
rotateY(calc(var(–flipped, 0) * 180deg));
}
Now strive shifting the stack! We’re getting there. 🙌
Animating the rings
We wish our ring to flip after which fall. A primary try may look one thing like this:
.ring {
animation-name: slink;
animation-duration: 2s;
animation-fill-mode: each;
animation-iteration-count: infinite;
}
@keyframes slink {
0%, 5% {
remodel:
translate3d(-50%, -50%, var(–origin-z))
translateZ(0)
rotateY(0deg);
}
25% {
remodel:
translate3d(-50%, -50%, var(–origin-z))
translateZ(0)
rotateY(180deg);
}
45%, 100% {
remodel:
translate3d(-50%, -50%, var(–origin-z))
translateZ(var(–destination-z))
rotateY(180deg);
}
}
Oof, that’s not proper in any respect!
However that’s solely as a result of we aren’t utilizing animation-delay. All of the rings are, um, slinking on the similar time. Let’s introduce an animation-delay based mostly on the –index of the ring so that they slink in succession.
.ring {
animation-delay: calc(var(–index) * 0.1s);
}
OK, that’s certainly “higher.” However the timing continues to be off. What stands out extra, although, is the shortcoming of animation-delay. It’s only utilized on the primary animation iteration. After that, we lose the impact.
At this level, let’s colour the rings so that they progress by way of the hue wheel. That is going to make it simpler to see what’s occurring.
.ring {
–hue: calc((360 / var(–ring-count)) * var(–index));
}
That’s higher! ✨
Again to the problem. As a result of we’re unable to specify a delay that’s utilized to each iteration, we’re additionally unable to get the impact we would like. For our Slinky, if we have been in a position to have a constant animation-delay, we’d be capable of obtain the impact we would like. And we may use one keyframe whereas counting on our scoped customized properties. Even an animation-repeat-delay could possibly be an fascinating addition.
This performance is accessible in JavaScript animation options. For instance, GreenSock means that you can specify a delay and a repeatDelay.
However, our Slinky instance isn’t the simplest factor as an example this drawback. Let’s break this down right into a fundamental instance. Take into account two containers. And also you need them to alternate spinning.
How will we do that with CSS and no “tips”? One thought is so as to add a delay to one of many containers:
.field {
animation: spin 1s var(–delay, 0s) infinite;
}
.field:nth-of-type(2) {
–delay: 1s;
}
@keyframes spin {
to {
remodel: rotate(360deg);
}
}
However, that gained’t work as a result of the pink field will preserve spinning. And so will the blue one after its preliminary animation-delay.
With one thing like GreenSock, although, we are able to obtain the impact we would like with relative ease:
import gsap from ‘https://cdn.skypack.dev/gsap’
gsap.to(‘.field’, {
rotate: 360,
/**
* A perform based mostly worth, implies that the primary field has a delay of 0 and
* the second has a delay of 1
*/
delay: (index) > index,
repeatDelay: 1,
repeat: -1,
ease: ‘power1.inOut’,
})
And there it’s!
However how can we do that with out JavaScript?
Properly, we’ve to “hack” our @keyframes and fully eliminate animation-delay. As an alternative, we are going to pad out the @keyframes with empty area. This comes with numerous quirks, however let’s go forward and construct a brand new keyframe first. This can totally rotate the aspect twice:
@keyframes spin {
50%, 100% {
remodel: rotate(360deg);
}
}
It’s like we’ve reduce the keyframe in half. And now we’ll need to double the animation-duration to get the identical velocity. With out utilizing animation-delay, we may strive setting animation-direction: reverse on the second field:
.field {
animation: spin 2s infinite;
}
.field:nth-of-type(2) {
animation-direction: reverse;
}
Nearly.
The rotation is the unsuitable approach spherical. We may use a wrapper aspect and rotate that, however that might get difficult as there are extra issues to steadiness. The opposite method is to create two keyframes as a substitute of 1:
@keyframes box-one {
50%, 100% {
remodel: rotate(360deg);
}
}
@keyframes box-two {
0%, 50% {
remodel: rotate(0deg);
}
100% {
remodel: rotate(360deg);
}
}
And there we’ve it:
This may’ve been loads simpler if we had a technique to specify the repeat delay with one thing like this:
/* Hypothetical! */
animation: spin 1s 0s 1s infinite;
Or if the repeated delay matched the preliminary delay, we may presumably have a combinator for it:
/* Hypothetical! */
animation: spin 1s 1s+ infinite;
It might make for an fascinating addition for certain!
So, we want keyframes for all these rings?
Sure, that’s, if we would like a constant delay. And we have to try this based mostly on what we’re going to use because the animation window. All of the rings have to have “slinked” and settled earlier than the keyframes repeat.
This may be horrible to write down out by hand. However because of this we’ve CSS preprocessors, proper? Properly, at the very least till we get loops and a few further customized property options on the internet. 😉
Right this moment’s weapon of alternative will probably be Stylus. It’s my favourite CSS preprocessor and has been for a while. Behavior means I haven’t moved to Sass. Plus, I like Stylus’s lack of required grammar and adaptability.
Good factor we solely want to write down this as soon as:
// STYLUS GENERATED KEYFRAMES BE HERE…
$ring-count = 10
$animation-window = 50
$animation-step = $animation-window / $ring-count
for $ring in (0..$ring-count)
// Generate a set of keyframes based mostly on the ring index
// index is the ring
$begin = $animation-step * ($ring + 1)
@keyframes slink-{$ring} {
// In right here is the place we have to generate the keyframe steps based mostly on ring rely and window.
0%, {$begin * 1%} {
remodel
translate3d(-50%, -50%, var(–origin-z))
translateZ(0)
rotateY(0deg)
}
// Flip with out falling
{($begin + ($animation-window * 0.75)) * 1%} {
remodel
translate3d(-50%, -50%, var(–origin-z))
translateZ(0)
rotateY(180deg)
}
// Fall till the cut-off level
{($begin + $animation-window) * 1%}, 100% {
remodel
translate3d(-50%, -50%, var(–origin-z))
translateZ(var(–destination-z))
rotateY(180deg)
}
}
Right here’s what these variables imply:
$ring-count: The variety of rings in our slinky.$animation-window: That is the proportion of the keyframe that we are able to slink in. In our instance, we’re saying we wish to slink over 50% of the keyframes. The remaining 50% ought to get used for delays.$animation-step: That is the calculated stagger for every ring. We are able to use this to calculate the distinctive keyframe percentages for every ring.
Right here’s the way it compiles to CSS, at the very least for the primary couple of iterations:
View full code
@keyframes slink-0 {
0%, 4.5% {
remodel:
translate3d(-50%, -50%, var(–origin-z))
translateZ(0)
rotateY(0deg);
}
38.25% {
remodel:
translate3d(-50%, -50%, var(–origin-z))
translateZ(0)
rotateY(180deg);
}
49.5%, 100% {
remodel:
translate3d(-50%, -50%, var(–origin-z))
translateZ(var(–destination-z))
rotateY(180deg);
}
}
@keyframes slink-1 {
0%, 9% {
remodel:
translate3d(-50%, -50%, var(–origin-z))
translateZ(0)
rotateY(0deg);
}
42.75% {
remodel:
translate3d(-50%, -50%, var(–origin-z))
translateZ(0)
rotateY(180deg);
}
54%, 100% {
remodel:
translate3d(-50%, -50%, var(–origin-z))
translateZ(var(–destination-z))
rotateY(180deg);
}
}
The very last thing to do is apply every set of keyframes to every ring. We are able to do that utilizing our markup if we would like by updating it to outline each an –index and a –name:
– const RING_COUNT = 10;
.container
.scene
.airplane(type=`–ring-count: ${RING_COUNT}`)
– let rings = 0;
whereas rings < RING_COUNT
.ring(type=`–index: ${rings}; –name: slink-${rings};`)
– rings++;
Which supplies us this when compiled:
<div class=”container”>
<div class=”scene”>
<div class=”airplane” type=”–ring-count: 10″>
<div class=”ring” type=”–index: 0; –name: slink-0;”></div>
<div class=”ring” type=”–index: 1; –name: slink-1;”></div>
<div class=”ring” type=”–index: 2; –name: slink-2;”></div>
<div class=”ring” type=”–index: 3; –name: slink-3;”></div>
<div class=”ring” type=”–index: 4; –name: slink-4;”></div>
<div class=”ring” type=”–index: 5; –name: slink-5;”></div>
<div class=”ring” type=”–index: 6; –name: slink-6;”></div>
<div class=”ring” type=”–index: 7; –name: slink-7;”></div>
<div class=”ring” type=”–index: 8; –name: slink-8;”></div>
<div class=”ring” type=”–index: 9; –name: slink-9;”></div>
</div>
</div>
</div>
After which our styling could be up to date accordingly:
.ring {
animation: var(–name) var(–speed) each infinite cubic-bezier(0.25, 0, 1, 1);
}
Timing is every part. So we’ve ditched the default animation-timing-function and we’re utilizing a cubic-bezier. We’re additionally making use of the –speed customized property we outlined initially.
Aw yeah. Now we’ve a slinking CSS Slinky! Have a play with a number of the variables within the code and see what completely different habits you may yield.
Creating an infinite animation
Now that we’ve the toughest half out of the way in which, we are able to make get this to the place the animation repeats infinitely. To do that, we’re going to translate the scene as our Slinky slinks so it seems to be like it’s slinking again into its unique place.
.scene {
animation: step-up var(–speed) infinite linear each;
}
@keyframes step-up {
to {
remodel: translate3d(-100%, 0, var(–depth));
}
}
Wow, that took little or no effort!
We are able to take away the platform colours from .scene and .airplane to forestall the animation from being too jarring:
Nearly finished! The very last thing to deal with is that the stack of rings flips earlier than it slinks once more. That is the place we talked about earlier that using colour would turn out to be useful. Change the variety of rings to an odd quantity, like 11, and change again to alternating the ring colour:
Increase! We now have a working CSS slinky! It’s configurable, too!
Enjoyable variations
How a couple of “flip flop” impact? By that, I imply getting the Slink to slink alternate methods. If we add an additional wrapper aspect to the scene, we may rotate the scene by 180deg on every slink.
– const RING_COUNT = 11;
.container
.flipper
.scene
.airplane(type=`–ring-count: ${RING_COUNT}`)
– let rings = 0;
whereas rings < RING_COUNT
.ring(type=`–index: ${rings}; –name: slink-${rings};`)
– rings++;
So far as animation goes, we are able to make use of the steps() timing perform and use twice the –speed:
.flipper {
animation: flip-flop calc(var(–speed) * 2) infinite steps(1);
top: 100%;
width: 100%;
}
@keyframes flip-flop {
0% {
remodel: rotate(0deg);
}
50% {
remodel: rotate(180deg);
}
100% {
remodel: rotate(360deg);
}
}
Final, however not least, let’s change the way in which the .scene aspect’s step-up animation works. It not wants to maneuver on the x-axis.
@keyframes step-up {
0% {
remodel: translate3d(-50%, 0, 0);
}
100% {
remodel: translate3d(-50%, 0, var(–depth));
}
}
Word the animation-timing-function that we use. That use of steps(1) is what makes it attainable.
In order for you one other enjoyable use of steps(), take a look at this #SpeedyCSSTip!
For an additional contact, we may rotate the entire scene gradual:
.container {
animation: rotate calc(var(–speed) * 40) infinite linear;
}
@keyframes rotate {
to {
remodel:
translate3d(0, 0, 100vmin)
rotateX(-24deg)
rotateY(-32deg)
rotateX(90deg)
translateZ(calc((var(–depth) + var(–stack-height)) * -1))
rotate(360deg);
}
}
I prefer it! After all, styling is subjective… so, I made a little bit app you should use configure your Slinky:
And listed here are the “Authentic” and “Flip-Flop” variations I took a little bit additional with shadows and theming.
Remaining demos
That’s it!
That’s at the very least one technique to make a pure CSS Slinky that’s each 3D and configurable. Certain, you may not attain for one thing like this day by day, however it brings up fascinating CSS animation strategies. It additionally raises the query of whether or not having a animation-repeat-delay property in CSS could be helpful. What do you assume? Do you assume there could be some good use circumstances for it? I’d like to know.
Make sure to have a play with the code — all of it’s accessible in this CodePen Assortment!
A CSS Slinky in 3D? Problem Accepted! initially revealed on CSS-Methods. It’s best to get the e-newsletter.
Subscribe to MarketingSolution.
Receive web development discounts & web design tutorials.
Now! Lets GROW Together!