Published on

Bring Your Site to Life with CSS Animations: A Step-by-Step Guide to Building a Ferris Wheel

Authors

'Bring Your Site to Life with CSS Animations: A Step-by-Step Guide to Building a Ferris Wheel'

Learn how to harness the power of pure CSS animations by building a beautiful, functional Ferris wheel from scratch. This comprehensive tutorial covers keyframes, transforms, and performance best practices.

Table of Contents

Bring Your Site to Life with CSS Animations: A Step-by-Step Guide to Building a Ferris Wheel

Web animation has evolved from a novelty to a cornerstone of modern user experience. When used thoughtfully, animations can guide user attention, provide feedback, and add a layer of polish that makes a website feel truly alive. While JavaScript libraries offer immense power, you might be surprised at what you can achieve with just CSS. Pure CSS animations are lightweight, performant, and surprisingly versatile.

Today, we're going to embark on a fun and practical project to showcase the power of CSS: we'll build a fully animated Ferris wheel from scratch. This project is perfect for understanding the fundamentals of CSS animations, including @keyframes, transform properties, and some clever positioning tricks. By the end, you'll not only have a charming animation to show off but also a deep understanding of the concepts you can apply to countless other projects.

Let's get started!

Part 1: The Foundations of CSS Animation

Before we start building, let's quickly review the core components of CSS animation. If you're already familiar with them, feel free to skim this section, but a quick refresher never hurts.

The magic of CSS animation revolves around two key pieces:

  1. The @keyframes at-rule: This is where you define the animation's steps. You specify what the styles should be at different points in the animation's timeline. You can use keywords from (0%) and to (100%) for simple animations, or use percentage markers (0%, 25%, 50%, 100%, etc.) for more complex sequences.

  2. Animation Properties: These are the CSS properties you apply to an element to tell it which animation to use and how it should run. The most common ones are:

    • animation-name: Specifies the name of the @keyframes rule to use.
    • animation-duration: Sets the length of time for one animation cycle (e.g., 10s, 500ms).
    • animation-timing-function: Dictates the speed curve of the animation. Common values are linear, ease-in, ease-out, and ease-in-out.
    • animation-iteration-count: Defines how many times the animation should repeat. infinite is a popular choice for continuous loops.
    • animation-direction: Determines if the animation should play forwards, backwards (reverse), or alternate between the two (alternate).

These can be combined into a single shorthand animation property, which we'll be using throughout this tutorial for cleaner code.

/* Example of the animation shorthand */
.my-element {
  animation: animation-name duration timing-function iteration-count direction;
}

Part 2: Scaffolding Our Ferris Wheel - The HTML Structure

Every great structure starts with a solid blueprint. For us, that's our HTML. We need a logical structure that represents the different parts of our Ferris wheel: the main wheel, the cabins, and a container to hold everything.

Let's keep it simple and semantic.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS Ferris Wheel</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="wheel-container">
        <div class="wheel">
            <div class="cabin-wrapper">
                <div class="cabin"></div>
            </div>
            <div class="cabin-wrapper">
                <div class="cabin"></div>
            </div>
            <div class="cabin-wrapper">
                <div class="cabin"></div>
            </div>
            <div class="cabin-wrapper">
                <div class="cabin"></div>
            </div>
            <div class="cabin-wrapper">
                <div class="cabin"></div>
            </div>
            <div class="cabin-wrapper">
                <div class="cabin"></div>
            </div>
        </div>
    </div>
</body>
</html>

Here's a breakdown of our structure:

  • .wheel-container: This is our main wrapper. It will help us center the Ferris wheel on the page and provide a positioning context.
  • .wheel: This is the star of the show—the large, circular structure that will rotate.
  • .cabin-wrapper: This is a crucial, non-visual element. Its job is to be positioned around the center of the wheel. We'll rotate these wrappers to place our cabins.
  • .cabin: This is the visual element that people would ride in. We'll nest it inside the wrapper to make our animations work correctly.

You might be wondering why we have a .cabin-wrapper. This is a common pattern in complex CSS layouts and animations. The wrapper handles the rotational positioning, while the inner .cabin element will handle the counter-rotational animation to keep it upright. Separating these concerns makes the CSS much easier to manage.

Part 3: Styling the Scene and Components

Now that we have our HTML, let's add some CSS to bring it from a list of divs to something that resembles a Ferris wheel.

First, let's set up our scene. We'll give the body a nice background and use flexbox to center our wheel container.

/* styles.css */
body {
    margin: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background: #87CEEB; /* A nice sky blue */
    font-family: sans-serif;
}

.wheel-container {
    position: relative;
    width: 500px;
    height: 500px;
}

Next, let's style the wheel itself. We'll make it a circle using border-radius: 50% and add a thick border to represent the metal frame. We'll also use position: relative to act as a container for our absolutely positioned cabins.

To add some detail, we can create spokes using pseudo-elements (::before and ::after). This is a neat trick to add visual flair without cluttering the HTML.

.wheel {
    position: relative;
    width: 100%;
    height: 100%;
    border: 10px solid #444;
    border-radius: 50%;
    box-sizing: border-box; /* Ensures border is inside the dimensions */
}

/* Creating spokes with pseudo-elements */
.wheel::before, .wheel::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: #444;
    z-index: -1;
}

.wheel::before {
    width: 10px;
    height: 100%;
}

.wheel::after {
    width: 100%;
    height: 10px;
}

/* We can add more spokes by adding more elements or pseudo-elements, 
but for now, this gives the right impression. */

Finally, let's style the cabins. We'll make them simple boxes for now.

.cabin {
    width: 60px;
    height: 60px;
    background: #C2185B; /* A nice magenta */
    border: 3px solid #444;
    border-radius: 5px;
}

At this point, you'll see a large circle with a cross in the middle, but all our cabin divs are nowhere to be seen (or they're stacked in the top-left corner). The next step is to position them correctly.

Part 4: Placing the Cabins with Transform and nth-child

This is where things get interesting. We need to arrange our six cabins evenly around the circumference of the wheel. We'll use absolute positioning for the .cabin-wrapper and then use the transform property to rotate each one into place.

The key is to place the transform-origin at the center of the wrapper and then push the cabin out.

Let's update our CSS:

.cabin-wrapper {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 60px; /* Same as cabin */
    height: 60px; /* Same as cabin */
    transform-origin: center center;
    
    /* This pushes the wrapper's center to the wheel's center */
    margin-top: -30px;
    margin-left: -30px;
}

.cabin {
    /* Now we position the cabin relative to its wrapper */
    position: absolute;
    /* Push it out to the edge of the wheel */
    transform: translateY(-250px); /* Half the wheel's height */
    transform-origin: center center;
}

With this setup, all six cabin-wrapper elements are stacked perfectly in the center of the wheel. And each .cabin is pushed 250px upwards from that central point. Now, we just need to rotate each .cabin-wrapper.

Since we have 6 cabins, we need to space them 360 / 6 = 60 degrees apart. The :nth-child selector is perfect for this!

/* Rotate each cabin wrapper into its final position */
.cabin-wrapper:nth-child(1) { transform: rotate(0deg); }
.cabin-wrapper:nth-child(2) { transform: rotate(60deg); }
.cabin-wrapper:nth-child(3) { transform: rotate(120deg); }
.cabin-wrapper:nth-child(4) { transform: rotate(180deg); }
.cabin-wrapper:nth-child(5) { transform: rotate(240deg); }
.cabin-wrapper:nth-child(6) { transform: rotate(300deg); }

If you check your browser now, you should see a static but perfectly constructed Ferris wheel! The cabins are all pointing towards the center, but that's okay—we'll fix that when we animate.

Part 5: The Main Event - Animating the Wheel's Rotation

It's time for the magic. Let's make this wheel spin!

First, we define our @keyframes for the rotation. We'll call it rotateWheel.

@keyframes rotateWheel {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(360deg);
    }
}

Simple, right? It just animates the transform property from 0 to 360 degrees.

Now, we apply this animation to our .wheel element using the shorthand property.

.wheel {
    /* ... existing styles ... */
    animation: rotateWheel 20s linear infinite;
}

Let's break down that shorthand:

  • rotateWheel: The name of our @keyframes rule.
  • 20s: The duration. The wheel will take 20 seconds to complete one full rotation.
  • linear: The timing function. This ensures a constant, steady speed, just like a real Ferris wheel. ease would make it speed up and slow down.
  • infinite: The iteration count. The animation will loop forever.

Refresh your page. The wheel is spinning! But... oh no! The cabins are spinning with it, tumbling our imaginary passengers around. This is expected behavior, as child elements inherit the transformations of their parents. Our next step is to counteract this.

Part 6: Keeping the Cabins Upright - The Counter-Rotation Trick

This is the most clever part of the entire build. To prevent the cabins from spinning along with the wheel, we need to apply an animation to them that rotates them in the opposite direction at the exact same speed.

First, let's define a new @keyframes rule for this counter-rotation.

@keyframes counterRotate {
    from {
        transform: translateY(-250px) rotate(0deg);
    }
    to {
        transform: translateY(-250px) rotate(-360deg);
    }
}

Notice two things here:

  1. We're rotating from 0 to -360 degrees—the opposite direction of the wheel.
  2. We're including the translateY(-250px) transform. This is crucial! If you only animate rotate, it will override the transform property we used for positioning. By including the translateY in the animation, we ensure the cabin stays in its correct outward position while it rotates.

Now, apply this animation to the .cabin element.

.cabin {
    /* ... existing styles ... */
    animation: counterRotate 20s linear infinite;
}

Crucially, the duration (20s) and timing function (linear) must match the wheel's animation exactly. This ensures the counter-rotation perfectly cancels out the parent's rotation at every point in the cycle.

Refresh your browser one more time. Voila! You have a beautiful, smoothly-animated Ferris wheel with cabins that stay perfectly upright as the wheel turns.

Part 7: Adding Polish and Best Practices

Our Ferris wheel is functional, but we can add a few finishing touches and discuss some important best practices.

Adding a Stand

Let's add a simple stand to make our Ferris wheel look more grounded. We can do this with pseudo-elements on the main .wheel-container.

.wheel-container::before, .wheel-container::after {
    content: '';
    position: absolute;
    bottom: -10px; /* Position at the bottom */
    background: #444;
    width: 10px;
    height: 150px;
    z-index: -2;
}

.wheel-container::before {
    left: 50%;
    transform: translateX(-50px) rotate(15deg);
}

.wheel-container::after {
    left: 50%;
    transform: translateX(40px) rotate(-15deg);
}

Performance Considerations: transform and opacity

We've been using transform to animate, and this was a deliberate choice. When you animate properties like transform and opacity, modern browsers can offload the work to the GPU (Graphics Processing Unit). This is called hardware acceleration.

Animating these properties only affects the browser's "Composite" step, which is very efficient. In contrast, animating properties that affect layout, like width, height, margin, or top/left, forces the browser to recalculate the layout and repaint the screen (Layout -> Paint -> Composite). This is much more computationally expensive and can lead to jerky, stuttering animations, especially on less powerful devices.

Best Practice: Whenever possible, stick to animating transform and opacity for the smoothest performance.

Accessibility: prefers-reduced-motion

While animations are great, they can be problematic for users with vestibular disorders, causing dizziness or nausea. It's our responsibility as developers to respect user preferences for reduced motion.

CSS provides a simple and powerful media query for this: prefers-reduced-motion.

We can use it to disable or tone down our animations for users who have this setting enabled in their operating system.

@media (prefers-reduced-motion: reduce) {
    .wheel, .cabin {
        animation: none;
    }
}

With this simple block of code, our animation will be disabled for users who prefer reduced motion, making our site more accessible and inclusive. You could also choose to slow the animation down instead of stopping it completely (e.g., animation-duration: 120s;).

Final Code

Here is the complete CSS for you to review and play with.

/* styles.css */
body {
    margin: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background: #87CEEB;
    font-family: sans-serif;
}

.wheel-container {
    position: relative;
    width: 500px;
    height: 500px;
}

.wheel-container::before, .wheel-container::after {
    content: '';
    position: absolute;
    bottom: -10px;
    background: #444;
    width: 10px;
    height: 150px;
    z-index: -2;
}

.wheel-container::before {
    left: 50%;
    transform: translateX(-50px) rotate(15deg);
}

.wheel-container::after {
    left: 50%;
    transform: translateX(40px) rotate(-15deg);
}

.wheel {
    position: relative;
    width: 100%;
    height: 100%;
    border: 10px solid #444;
    border-radius: 50%;
    box-sizing: border-box;
    animation: rotateWheel 20s linear infinite;
}

.wheel::before, .wheel::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: #444;
    z-index: -1;
}

.wheel::before {
    width: 10px;
    height: 100%;
}

.wheel::after {
    width: 100%;
    height: 10px;
}

.cabin-wrapper {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 60px;
    height: 60px;
    margin-top: -30px;
    margin-left: -30px;
    transform-origin: center center;
}

.cabin {
    position: absolute;
    width: 60px;
    height: 60px;
    background: #C2185B;
    border: 3px solid #444;
    border-radius: 5px;
    transform-origin: center center;
    transform: translateY(-250px);
    animation: counterRotate 20s linear infinite;
}

.cabin-wrapper:nth-child(1) { transform: rotate(0deg); }
.cabin-wrapper:nth-child(2) { transform: rotate(60deg); }
.cabin-wrapper:nth-child(3) { transform: rotate(120deg); }
.cabin-wrapper:nth-child(4) { transform: rotate(180deg); }
.cabin-wrapper:nth-child(5) { transform: rotate(240deg); }
.cabin-wrapper:nth-child(6) { transform: rotate(300deg); }

@keyframes rotateWheel {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

@keyframes counterRotate {
    from { transform: translateY(-250px) rotate(0deg); }
    to { transform: translateY(-250px) rotate(-360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .wheel, .cabin {
        animation: none;
    }
}

Conclusion

Congratulations! You've successfully built a complex, multi-part animation using only HTML and CSS. In the process, you've learned how to:

  • Structure HTML for complex animations.
  • Use @keyframes to define animation sequences.
  • Apply animations and control their behavior.
  • Leverage transform, position, and nth-child to create intricate layouts.
  • Master the counter-rotation trick to create sophisticated mechanical motion.
  • Consider performance and accessibility in your animations.

These concepts are fundamental building blocks. You can now take them and create your own amazing things. Try changing the number of cabins, adjusting the speed, adding animations to the cabins themselves (like blinking lights!), or designing a completely different animated object. The possibilities are endless.

Happy coding!