Braydon Coyer just lately launched a month-to-month CSS artwork problem. He truly had reached out to me about donating a replica of my e-book Transfer Issues with CSS to make use of as a prize for the winner of the problem — which I used to be very happy 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 traditional toy you knock down the steps and it travels with its personal momentum.
Can we create a Slinky strolling down stairs like that in CSS? That’s precisely the type of problem I like, so I believed we may sort out that collectively on this article. Able to roll? (Pun meant.)
Establishing the Slinky HTML
Let’s make this versatile. (No pun meant.) What I imply by that’s we would like to have the ability to management the Slinky’s habits by 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(model=`--ring-count: ${RING_COUNT}`)
- let rings = 0;
whereas rings < RING_COUNT
.ring(model=`--index: ${rings};`)
- rings++;
These inline customized properties are a simple manner for us to replace the variety of rings and can come in useful as we get deeper into this problem. The code above provides us 10
rings with HTML that appears one thing like this when compiled:
<div class="container">
<div class="scene">
<div class="airplane" model="--ring-count: 10">
<div class="ring" model="--index: 0;"></div>
<div class="ring" model="--index: 1;"></div>
<div class="ring" model="--index: 2;"></div>
<div class="ring" model="--index: 3;"></div>
<div class="ring" model="--index: 4;"></div>
<div class="ring" model="--index: 5;"></div>
<div class="ring" model="--index: 6;"></div>
<div class="ring" model="--index: 7;"></div>
<div class="ring" model="--index: 8;"></div>
<div class="ring" model="--index: 9;"></div>
</div>
</div>
</div>
The preliminary Slinky CSS
We’re going to want some types! What we would like is a three-dimensional scene. I’m aware 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’d like types for our .scene
. The trick is to translate the .airplane
so it appears like our CSS Slinky is transferring infinitely down a flight of stairs. I needed to mess around to get issues precisely the way in which I need, 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 {
/* Coloration is bigoted */
background: rgb(162 25 230 / 0.25);
}
.airplane {
/* Coloration 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 utilizingperspective
at this degree, so we are able to get away with it.rotateX(-24deg) rotateY(32deg)
: This rotates the scene primarily based 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 will use this to maneuver the scene and heart 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 heart issues.rotate(0deg)
: Though, not in use for the time being, 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) strong var(--accent);
place: absolute;
prime: 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. After I first applied this, I wished to create a rainbow slinky the place the rings went by the hues. However that provides a little bit of complexity to the impact.
Now we’ve obtained some rings on our raised .airplane
:
Remodeling the Slinky rings
It’s time to get issues transferring! You’ll have observed that we set a transform-origin
on every .ring
like this:
.ring {
transform-origin: calc(100% + (var(--scene-size) * 0.2)) 50%;
}
That is primarily based on the .scene
measurement. That 0.2
worth is half the remaining out there measurement 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
? Effectively, we’d like the ring to seem like is transferring off-center. Enjoying with the remodel
of a person ring is an effective method 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 subsequent stair. How can we make every ring fall to the suitable place?
Effectively, we’ve got a calculated --origin-z
, so let’s calculate --destination-z
so the depth modifications because the rings remodel
. If we’ve got a hoop on prime of the stack, it ought to wind up on the backside after it falls. We will 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 transferring the stack! We’re getting there. 🙌
Animating the rings
We would like our ring to flip after which fall. A primary try would possibly 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 identical time. Let’s introduce an animation-delay
primarily based on the --index
of the ring in order that they slink in succession.
.ring {
animation-delay: calc(var(--index) * 0.1s);
}
OK, that’s certainly “higher.” However the timing remains to be off. What stands proud extra, although, is the shortcoming of animation-delay
. It is just utilized on the primary animation iteration. After that, we lose the impact.
At this level, let’s coloration the rings in order that they progress by 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 attention-grabbing addition.
This performance is obtainable in JavaScript animation options. For instance, GreenSock lets you specify a delay
and a repeatDelay
.
However, our Slinky instance isn’t the best factor as an example this drawback. Let’s break this down right into a fundamental instance. Take into account two packing containers. And also you need them to alternate spinning.
How can we do that with CSS and no “methods”? One concept is so as to add a delay to one of many packing containers:
.field {
animation: spin 1s var(--delay, 0s) infinite;
}
.field:nth-of-type(2) {
--delay: 1s;
}
@keyframes spin {
to {
remodel: rotate(360deg);
}
}
However, that received’t work as a result of the crimson 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 operate primarily based 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?
Effectively, we’ve got to “hack” our @keyframes
and utterly dispose of animation-delay
. As a substitute, we’ll pad out the @keyframes
with empty house. This comes with varied quirks, however let’s go forward and construct a brand new keyframe first. It will totally rotate the component 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 pace. 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;
}
Virtually.
The rotation is the incorrect manner spherical. We may use a wrapper component and rotate that, however that would get difficult as there are extra issues to steadiness. The opposite strategy 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 got it:
This could’ve been rather a lot simpler if we had a method 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 probably have a combinator for it:
/* Hypothetical! */
animation: spin 1s 1s+ infinite;
It might make for an attention-grabbing addition for certain!
So, we’d like keyframes for all these rings?
Sure, that’s, if we would like a constant delay. And we have to do this primarily based on what we’re going to use because the animation window. All of the rings must have “slinked” and settled earlier than the keyframes repeat.
This could be horrible to jot down out by hand. However for this reason we’ve got CSS preprocessors, proper? Effectively, a minimum of till we get loops and a few additional customized property options on the internet. 😉
As we speak’s weapon of alternative might 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 jot 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 primarily based 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 primarily based 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 over50%
of the keyframes. The remaining50%
ought to get used for delays.$animation-step
: That is the calculated stagger for every ring. We will use this to calculate the distinctive keyframe percentages for every ring.
Right here’s the way it compiles to CSS, a minimum of 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 will 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(model=`--ring-count: ${RING_COUNT}`)
- let rings = 0;
whereas rings < RING_COUNT
.ring(model=`--index: ${rings}; --name: slink-${rings};`)
- rings++;
Which supplies us this when compiled:
<div class="container">
<div class="scene">
<div class="airplane" model="--ring-count: 10">
<div class="ring" model="--index: 0; --name: slink-0;"></div>
<div class="ring" model="--index: 1; --name: slink-1;"></div>
<div class="ring" model="--index: 2; --name: slink-2;"></div>
<div class="ring" model="--index: 3; --name: slink-3;"></div>
<div class="ring" model="--index: 4; --name: slink-4;"></div>
<div class="ring" model="--index: 5; --name: slink-5;"></div>
<div class="ring" model="--index: 6; --name: slink-6;"></div>
<div class="ring" model="--index: 7; --name: slink-7;"></div>
<div class="ring" model="--index: 8; --name: slink-8;"></div>
<div class="ring" model="--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 the whole lot. 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 at first.
Aw yeah. Now we’ve got a slinking CSS Slinky! Have a play with a few of the variables within the code and see what completely different habits you may yield.
Creating an infinite animation
Now that we’ve got 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 appears like it’s slinking again into its authentic 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 will take away the platform colours from .scene
and .airplane
to forestall the animation from being too jarring:
Virtually carried out! 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 coloration would come in useful. Change the variety of rings to an odd quantity, like 11
, and change again to alternating the ring coloration:
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 component to the scene, we may rotate the scene by 180deg
on every slink.
- const RING_COUNT = 11;
.container
.flipper
.scene
.airplane(model=`--ring-count: ${RING_COUNT}`)
- let rings = 0;
whereas rings < RING_COUNT
.ring(model=`--index: ${rings}; --name: slink-${rings};`)
- rings++;
So far as animation goes, we are able to make use of the steps()
timing operate 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
component’s step-up
animation works. It now not wants to maneuver on the x-axis.
@keyframes step-up {
0% {
remodel: translate3d(-50%, 0, 0);
}
100% {
remodel: translate3d(-50%, 0, var(--depth));
}
}
Observe the animation-timing-function
that we use. That use of steps(1)
is what makes it attainable.
If you need one other enjoyable use of steps()
, try 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 slightly app you need to use configure your Slinky:
And listed here are the “Unique” and “Flip-Flop” variations I took slightly additional with shadows and theming.
Ultimate demos
That’s it!
That’s a minimum of one method to make a pure CSS Slinky that’s each 3D and configurable. Positive, you may not attain for one thing like this each day, nevertheless it brings up attention-grabbing 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 suppose? Do you suppose there could be some good use instances for it? I’d like to know.
Make sure you have a play with the code — all of it’s out there in this CodePen Assortment!