Why Complex Animations Are Simpler Than They Sound

Advanced CSS animations are often just several simple animations stacked together. Two ideas unlock this: easing functions, especially cubic-bezier(), and the ability to sequence animations with animation-delay. This article walks through both and builds a complete rollercoaster path for a ball — a slide, a horizontal pause, and a loop — using only these techniques.

Before continuing, ensure you are comfortable with basic CSS animations like @keyframes and animation shorthand. If not, review a beginner CSS animation guide first.

Understanding Cubic Bézier Curves

The CSS cubic-bezier() function is an easing function. It defines how an animation progresses over time. According to the W3C specification:

A cubic Bézier easing function is a type of easing function defined by four real numbers that specify the two control points, P1 and P2, of a cubic Bézier curve whose end points P0 and P3 are fixed at (0, 0) and (1, 1) respectively. The x coordinates of P1 and P2 are restricted to the range [0, 1].
The cubic-bezier function
(Large preview)

In practical terms, easing functions control the velocity profile of an animation. The default is often a linear curve, where an object moves at a constant speed from start to finish. Consider two points, P0 (start) and P1 (end), with a point moving between them at a fixed rate.

Animation of a linear Bezier curve
Source: Wikipedia

From Linear To Quadratic

Add a third point, P1, acting as a control point between start P0 and end P2. This creates a quadratic curve. The mechanics:

  1. Draw imaginary lines from P0 to P1 and P1 to P2.
  2. A point Q0 moves along the first line, and Q1 along the second.
  3. Connect Q0 and Q1 with an imaginary line.
  4. As Q0 and Q1 move, a point B travels along this connecting line, tracing the final path.
Construction of a quadratic Bézier curve
Source: Wikipedia (Large preview)
Animation of a quadratic Bézier curve, t in [0,1]
Source: Wikipedia

These points (Q1, Q2, B) don’t move at the same velocity. They all start and finish at the same time, so their speeds adjust according to the length of the line they traverse.

The Cubic Case

The cubic curve uses four points: P0 (start) and P3 (end), with two control points P1 and P2.

Construction of a cubic Bézier curve
Source: Wikipedia (Large preview)

The process stacks one level deeper:

  1. Connect lines between (P0, P1), (P1, P2), and (P2, P3).
  2. Points Q0, Q1, and Q2 move along those three lines respectively.
  3. Connect (Q0, Q1) and (Q1, Q2), and place R0 and R1 moving along these two new lines.
  4. Finally, connect R0 and R1, and a point B moving along this last line creates the animation curve.
Animation of a cubic Bezier curve, t in [0,1]
Source: Wikipedia

Experiment with interactive cubic bezier tools, such as a Desmos graph, to see how moving the control points (P1, P2) changes the resulting path.

The Art of Animation Stacking

The key to large animation sequences is decomposition. Every major movement can be broken into smaller, individual animations and scheduled back-to-back. The scheduler is the animation-delay property, calculated as the total duration of all animations that run before the one in question.

Suppose an element has two animations: movePointLeft (2s) and movePointDown (2s).

animation: movePointLeft 4s linear forwards, movePointDown 3s linear forwards;

Since movePointLeft is first, its delay is 0. movePointDown must wait 4 seconds until the first is complete.

animation-delay: 0s, 4s;

When two or more animations fire concurrently, their delays match, and you count that period as a single block for calculating later delays. For example, with an x animation and a y animation running simultaneously:

animation: x 4s linear forwards, y 4s linear forwards, jump 2s linear forwards;

Both x and y begin at 0. A subsequent jump animation gets a delay of 4 seconds, not 8.

animation-delay: 0s, 0s, 4s;

Building The Rollercoaster

The path is assembled from three logical parts: a slide along a curve, a short horizontal shift for breathing room, and a 100px-radius loop. The journey is best illustrated as a sequence:

The rollercoaster path
(Large preview)

Setup

Create an HTML file and place a single div for the ball in the body:

<div id="the-cart" class="cart"></div>

The initial CSS sets the ball’s size and styling. Viewport units (vw, vh) help maintain responsiveness:

.cart {
  background-color: rgb(100, 210, 128);
  height: 50px;
  width: 50px;
  border: 1px solid black;
  border-radius: 50px;
  position: absolute;
  left: 10vw;
  top: 30vh;
}

Feel free to use any units you prefer. The rest of the CSS will target the ball’s animations.

Phase 1: The Sliding Path

The slide is realized through two separate animations: one for the horizontal (x-axis) component and another for the vertical (y-axis) component, which carries the cubic bezier.

The horizontal motion is linear:

@keyframes x {
  to {
    left: 40vw;
  }
}

This gets added to the ball’s animation property:

animation: x 4s linear forwards

For the vertical part, the custom cubic-bezier creates the slide feel. The keyframes keep the start and end top values almost identical, so the ball returns near its original height:

@keyframes y {
  to {
    top: 29.99vh;
  }
}}

The function’s control points determine the feel: the animation should creep forward first, then rush downward in a slide. This requires P1 on the x-axis; a value around 0.55 works. To get the slide, P2 must sit well below the baseline on the y-axis. A large negative value like -800 creates the fast drop, and returning P2 near 0 on the x-axis makes the slide steeper. The final function is cubic-bezier(0.55, 0, 0.2, -800).

Apply the keyframes and timeline:

animation: x 4s linear forwards,
    y 4s cubic-bezier(0.55, 0, 0.2, -5000) forwards;

Sliding consumes four seconds, so the preceding animations’ delays are zero, and following ones must wait:

animation-delay: 0s, 0s;

See the Pen [Rollercoaster sliding part [forked]](https://codepen.io/smashingmag/pen/VwxXBQb) by Yosra Emad.

See the Pen Rollercoaster sliding part [forked] by Yosra Emad.

Phase 2: Horizontal Buffer

Before the loop, the ball needs a horizontal push to create some space. This animation operates only on the x-axis over two seconds:

@keyframes x2 {
  to {
    left: 50vw;
  }
}

Register this keyframe set:

animation: x 4s linear forwards,
    y 4s cubic-bezier(0.55, 0, 0.2, -5000) forwards, x2 0.5s linear forwards;

Since the slide took four seconds, the delay is set exactly to that:

animation-delay: 0s, 0s, 4s;

See the Pen [Rollercoaster horizontal space [forked]](https://codepen.io/smashingmag/pen/dyemExY) by Yosra Emad.

See the Pen Rollercoaster horizontal space [forked] by Yosra Emad.

Phase 3: The Loop

Animating a circular loop requires the ball to rotate around a center point. The center is offset horizontally, so the ball must reposition to the loop’s middle first. This position shift is an instant change (0s) scheduled immediately after the buffer path. Here, the ball’s top becomes 20vh — representing the loop’s center relative to the ball’s 10vh radius:

@keyframes pointOfCircle {
  to {
    top: 20vh;
  }
}

The position animation is added with zero duration:

animation: x 4s linear forwards,
    y 4s cubic-bezier(0.55, 0, 0.2, -5000) forwards, x2 0.5s linear forwards,
    pointOfCircle 0s linear forwards;

Its delay is the sum of the slide (4s) and buffer (0.5s), totalling 4.5s:

animation-delay: 0s, 0s, 4s, 4.5s;

Completing the Revolution

The actual loop animation returns the ball to its previous vertical position while rotating it through 360 degrees:

@keyframes loop {
  from {
    transform: rotate(0deg) translateY(10vh) rotate(0deg);
  }
  to {
    transform: rotate(-360deg) translateY(10vh) rotate(360deg);
  }
}

The `loop` animation is added, with the same 4.5s delay, taking a total of 3 seconds to complete:

animation: x 4s linear forwards,
    y 4s cubic-bezier(0.55, 0, 0.2, -5000) forwards, x2 0.5s linear forwards,
    pointOfCircle 0s linear forwards, loop 3s linear forwards;
animation-delay: 0s, 0s, 4s, 4.5s, 4.5s;

See the Pen [Rollercoaster loop [forked]](https://codepen.io/smashingmag/pen/mdLxZdR) by Yosra Emad.

See the Pen Rollercoaster loop [forked] by Yosra Emad.

Phase 4: Exit Space

The animation shouldn’t end abruptly where the loop ends. A final horizontal impulse moves the ball away cleanly, matching the start of the loop’s rotation path:

@keyframes x3 {
  to {
    left: 70vw;
  }
}

With all previous sequences (slide 4s, buffer 0.5s, loop 3s), this last keyframe is delayed by 7.5s:

animation: x 4s linear forwards,
    y 4s cubic-bezier(0.55, 0, 0.2, -800) forwards, x2 0.5s linear forwards,
    pointOfCircle 0s linear forwards, loop 3s linear forwards,
    x3 2s linear forwards;
animation-delay: 0s, 0s, 4s, 4.5s, 4.5s, 7.5s;

Final Output

The full sequence demonstrates that a rollercoaster isn’t one animation but a carefully timed composition of many:

See the Pen [Rollercoaster Final [forked]](https://codepen.io/smashingmag/pen/wvjmLKp) by Yosra Emad.

See the Pen Rollercoaster Final [forked] by Yosra Emad.

What seems like a single complex motion is just multiple elementary animations orchestrated to play in sequence using animation-delay. Crafting bespoke easing functions with cubic-bezier() gives you precise dynamic control over each segment. The best way to internalize these patterns is to sketch your own multi-step path and build it out keyframe by keyframe.

Smashing Editorial