Blog

Animations in HTML and CSS.

by Bojan Petrovic

Space tourism might be the next big thing. And, although there are some rumors about a possibility to see some not-so-distant planets up close and personal, we believe there is still some waiting involved. And since we are not so big on waiting, we decided to make our own solar system using only HTML and CSS. We used CSS animations and now we can watch all those planets dance around the Sun and play with them. Feel free to explore our galaxy.

Creating the solar system markup

Let's get started. First we need to create our solar system. We'll rely on the BEM naming convention to make our galaxy cleaner and reusable.

<div class="galaxy">
    <div class="solarSystem">
        <div class="sun"></div>

        <div class="orbit orbit--earth">
            <div class="planet planet--earth">
                <div class="orbit orbit--moon">
                    <div class="moon"></div>
                </div>
            </div>
        </div>
    </div>
</div>

Setting up the variables

For this little project we will use the power of Sass. To keep the code maintainable we will use variables. For now, we will define variables for orbit and planet sizes (not to scale).

// Sun & Moon Size
$sun: 70px;
$moon: 7px;

// Orbit Sizes
$earth-orbit: 250px;
$moon-orbit: 50px;

// Planet Sizes
$earth-planet: 25px;

// Background Colors
$moon-bg-color: #ccc;

Styling the orbits

It's styling time! First, we'll start with orbits.

.orbit {
  position: absolute;
  top: 50%;
  left: 50%;

  border: 1px solid rgba(#fff, .8);
  border-radius: 50%;
}

Sass maps

Variables are not the only useful thing in Sass. In this example, all of our orbits have some of the same properties, and we don't want to be repetitive in our code. That's where maps come handy!

A map represent an association between keys and values, similar to an object or an associative array.

$orbits: (
  "earth": (
    width: $earth-orbit,
    height: $earth-orbit,
    marginTop: -$earth-orbit / 2,
    marginLeft: -$earth-orbit / 2,
  ),
  "moon": (
    width: $moon-orbit,
    height: $moon-orbit,
    marginTop: -$moon-orbit / 2,
    marginLeft: -$moon-orbit / 2,
  ),
);

@each $name, $orbit in $orbits {
  .orbit--#{$name} {
    width: map-get($orbit, width);
    height: map-get($orbit, height);

    margin-top: map-get($orbit, marginTop);
    margin-left: map-get($orbit, marginLeft);
  }
}

In the @each loop above, we're cycling over each key/value pair in $orbits, assigning the class name based on the $orbit key. We will use maps for styling planets too.

Styling the planets

Once we've styled the orbits, it's time to do the same with the Sun, the Moon and the planets.

.sun,
.planet,
.moon {
  position: absolute;
  top: 50%;
  left: 50%;

  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;

  border-radius: 50%;
}

.sun {
  width: $sun;
  height: $sun;

  margin-top: -$sun / 2;
  margin-left: -$sun / 2;

  background-image: url("/images/sun.png");
}

.moon {
  left: -$moon / 2;

  width: $moon;
  height: $moon;

  background: $moon-bg-color;
}

$planets: (
  "earth": (
    left: -$earth-planet / 2,
    width: $earth-planet,
    height: $earth-planet,
    backgroundImage: url("/images/earth.jpg"),
  ),
);

@each $name, $planet in $planets {
  .planet--#{$name} {
    left: map-get($planet, left);

    width: map-get($planet, width);
    height: map-get($planet, height);

    background-image: map-get($planet, backgroundImage);
  }
}

It's time to animate!

Now that we have everything set up, it's time to animate the planet movement around the Sun, and the Moon around the planet.

Orbit animation

To achieve the effect of a planet moving around the Sun, we will animate the orbit itself and not just the planet. We will animate the planets later, once we're happy with the orbit animation.

@keyframes animate--orbit {
  0% {
    transform: rotateZ(0deg);
  }

  100% {
    transform: rotateZ(-360deg);
  }
}

Adding animation properties

The animation is ready! The only thing we need is to add animation properties to our orbits.

.orbit {
  // ...

  animation-name: animate--orbit;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.orbit--earth {
  // ...

  animation-duration: 10s;
}

.orbit--moon {
  // ...

  animation-duration: 4s;
}

Adding animation properties

The animation is ready! The only thing we need is to add animation properties to our orbits.

.orbit {
  // ...

  animation-name: animate--orbit;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@each $name, $orbit in $orbits {
  .orbit--#{$name} {
    // ...

    animation-duration: map-get($planet, animationDuration);
  }
}

It's 3D time!

We have the animated 2D solar system ready to go and we could say we are done! If we were doing iOS animations, we could stop here and act all smug, but let's take it a step further and make everything 3D!

To get the 3D effect, we will rotate the whole solar system and all the planets that are part of it.

.solarSystem {
  position: absolute;

  width: 100%;
  height: 100%;

  transform: rotateX(75deg);

  // Indicates that the children of the element should 
  // be positioned in the 3D-space.
  transform-style: preserve-3d;
}

.sun {
  // ...

  transform: rotateX(-90deg);
}

To get the 3D effect on planets, we need to animate them. To do so, we need to rotate the planet in the opposite direction of the orbit rotation.

@keyframes animate--planet {
  0% {
    transform: rotateX(-90deg) rotateY(360deg) rotateZ(0deg);
  }

  100% {
    transform: rotateX(-90deg) rotateY(0deg) rotateZ(0deg);
  }
}

It seems that our Moon orbit looks a bit strange now that we have rotated our planet. In order to make it right, we need to rotate the X axis in the opposite direction yet again.

@keyframes animate--suborbit {
  0% {
    transform: rotateX(90deg) rotateZ(0deg);
  }

  100% {
    transform: rotateX(90deg) rotateZ(-360deg);
  }
}

Let's add animation properties to our planets.

@each $name, $planet in $planets {
  .planet--#{$name} {
    // ...

    animation-name: map-get($planet, animationName);
    animation-duration: map-get($planet, animationDuration);
  }
}

Calculating planet revolution in seconds

Let's calculate the time the planet needs to go around the Sun. To do this, we will use a Sass function that we'll call revolution.

// The number of seconds an Earth year should take. The animation would take a
// long time and be pretty boring if we used the actual duration here.
$year-in-seconds: 30;

// The number of days it takes for the Earth to revolve around the Sun.
$earth-revolution-days: 365.2563;

// Function for calculating planet revolution in Earth years.
@function revolution($planet-year-in-earth-days) {
  @return $year-in-seconds * $planet-year-in-earth-days / $earth-revolution-days + s;
}

It's time to use our Sass function to define the animation-duration property.

.planet--earth {
  // ...

  animation-duration: revolution($earth-revolution-days);
}

Making everything cooler

We are almost done. We already have a pretty cool 3D animation, but why don't we make it even cooler? To do so, we will create dynamic shadows with CSS animations!

// Shadow variable
$shadow-color: rgba(#000, .6);

$shadows: (
  "earth": (
    0%: inset 8px 0 5px $shadow-color,
    25%: inset 45px -20px 25px $shadow-color,
    25.001%: inset -45px -20px 25px $shadow-color,
    50%: inset -8px 0 5px $shadow-color,
    75%: inset -2px 3px 2px $shadow-color,
    100%: inset 8px 0 5px $shadow-color,
  ),
);

@each $name, $shadow in $shadows {
  @keyframes shadow-#{$name} {
    // From 0% to 25%, the shadow appears from left to right.
    0% {
      box-shadow: map-get($shadow, 0%);
    }

    25% {
      box-shadow: map-get($shadow, 25%);
    }
    
	// From 25.001% to 100%, the shadow disappears from left to
    25.001% {
      box-shadow: map-get($shadow, 25.001%);
    }

    50% {
      box-shadow: map-get($shadow, 50%);
    }

    75% {
      box-shadow: map-get($shadow, 75%);
    }
    
	// The shadow is now back to the initial position.
    100% {
      box-shadow: map-get($shadow, 100%);
    }
  }
}

Now we have our small solar system with planet Earth and its Moon. As we all know, we are not alone in the system and we should add other planets, too.

<div class="galaxy">
    <div class="solarSystem">
        <div class="sun"></div>

        <div class="orbit orbit--mercury">
            <div class="planet planet--mercury"></div>
        </div>

        <div class="orbit orbit--venus">
            <div class="planet planet--venus"></div>
        </div>

        <div class="orbit orbit--earth">
            <div class="planet planet--earth">
                <div class="orbit orbit--moon">
                    <div class="moon"></div>
                </div>
            </div>
        </div>

        <div class="orbit orbit--mars">
            <div class="planet planet--mars"></div>
        </div>

        <div class="orbit orbit--jupiter">
            <div class="planet planet--jupiter"></div>
        </div>

        <div class="orbit orbit--saturn">
            <div class="planet planet--saturn"></div>
        </div>

        <div class="orbit orbit--uranus">
            <div class="planet planet--uranus"></div>
        </div>

        <div class="orbit orbit--neptune">
            <div class="planet planet--neptune"></div>
        </div>
    </div>
</div>

All planets are here now, and we can play with our variables to make them different sizes. We can also set different distances from the Sun for each planet. Going even further, we can set how many days planets need to go around the Sun!

// Function for calculating planet revolution in Earth years.
@function revolution($planet-year-in-earth-days) {
  @return $year-in-seconds * $planet-year-in-earth-days / $earth-revolution-days + s;
}

/**
 * Variables.
 */

// The number of seconds an Earth year should take. The animation would take a
// long time and be pretty boring if we used the actual duration here.
$year-in-seconds: 30;

// Sun & Moon Size
$sun: 70px;
$moon: 4px;

// Orbit Sizes
$mercury-orbit: 120px;
$venus-orbit: 175px;
$earth-orbit: 255px;
$moon-orbit: 50px;
$mars-orbit: 335px;
$jupiter-orbit: 595px;
$saturn-orbit: 770px;
$uranus-orbit: 910px;
$neptune-orbit: 1100px;

// Planet Sizes
$mercury-planet: 10px;
$venus-planet: 24px;
$earth-planet: 26px;
$mars-planet: 14px;
$jupiter-planet: 71px;
$saturn-planet: 60px;
$uranus-planet: 26px;
$neptune-planet: 25px;

// Background Colors
$moon-bg-color: #ccc;
$saturn-ring-bg-color: #a09382;

// Shadows
$shadow-color: rgba(#000, .6);

// Planet Revolution in Days
$mercury-revolution-days: 87.5;
$venus-revolution-days: 224.7;
$earth-revolution-days: 365.2563;
$moon-revolution-days: 27.3216;
$mars-revolution-days: 687;
$jupiter-revolution-days: 4331;
$saturn-revolution-days: 10747;
$uranus-revolution-days: 30589;
$neptune-revolution-days: 59802;

/**
 * Styling.
 */

* {
  box-sizing: border-box;
}

html,
body {
  width: 100%;
  height: 100%;

  padding: 0;
  margin: 0;
  overflow: hidden;

  background-image: url("/images/galaxy.jpg");
}


.galaxy {
  position: relative;

  width: 100%;
  height: 100%;
}

.solarSystem {
  position: absolute;

  width: 100%;
  height: 100%;

  transform: rotateX(75deg);
  transform-style: preserve-3d;
}

/**
* Orbit styles.
 */

.orbit {
  position: absolute;

  top: 50%;
  left: 50%;

  border: 1px solid rgba(#fff, .2);
  border-radius: 50%;

  animation-name: animate--orbit;
  animation-timing-function: linear;
  animation-iteration-count: infinite;

  transform-style: preserve-3d;
}

$orbits: (
  "mercury": (
    width: $mercury-orbit,
    height: $mercury-orbit,
    marginTop: -$mercury-orbit / 2,
    marginLeft: -$mercury-orbit / 2,
    animationDuration: revolution($mercury-revolution-days),
  ),
  "venus": (
    width: $venus-orbit,
    height: $venus-orbit,
    marginTop: -$venus-orbit / 2,
    marginLeft: -$venus-orbit / 2,
    animationDuration: revolution($venus-revolution-days),
  ),
  "earth": (
    width: $earth-orbit,
    height: $earth-orbit,
    marginTop: -$earth-orbit / 2,
    marginLeft: -$earth-orbit / 2,
    animationDuration: revolution($earth-revolution-days),
  ),
  "mars": (
    width: $mars-orbit,
    height: $mars-orbit,
    marginTop: -$mars-orbit / 2,
    marginLeft: -$mars-orbit / 2,
    animationDuration: revolution($mars-revolution-days),
  ),
  "jupiter": (
    width: $jupiter-orbit,
    height: $jupiter-orbit,
    marginTop: -$jupiter-orbit / 2,
    marginLeft: -$jupiter-orbit / 2,
    animationDuration: revolution($jupiter-revolution-days),
  ),
  "saturn": (
    width: $saturn-orbit,
    height: $saturn-orbit,
    marginTop: -$saturn-orbit / 2,
    marginLeft: -$saturn-orbit / 2,
    animationDuration: revolution($saturn-revolution-days),
  ),
  "uranus": (
    width: $uranus-orbit,
    height: $uranus-orbit,
    marginTop: -$uranus-orbit / 2,
    marginLeft: -$uranus-orbit / 2,
    animationDuration: revolution($uranus-revolution-days),
  ),
  "neptune": (
    width: $neptune-orbit,
    height: $neptune-orbit,
    marginTop: -$neptune-orbit / 2,
    marginLeft: -$neptune-orbit / 2,
    animationDuration: revolution($neptune-revolution-days),
  ),
  "moon": (
    width: $moon-orbit,
    height: $moon-orbit,
    marginTop: -$moon-orbit / 2,
    marginLeft: -$moon-orbit / 2,
    animationDuration: revolution($moon-revolution-days),
  ),
);

@each $name, $orbit in $orbits {
  .orbit--#{$name} {
    width: map-get($orbit, width);
    height: map-get($orbit, height);

    margin-top: map-get($orbit, marginTop);
    margin-left: map-get($orbit, marginLeft);

    animation-duration: map-get($orbit, animationDuration);
  }
}

.orbit--moon {
  animation-name: animate--suborbit;
}

/**
 * Planet styles.
 */

.sun,
.planet,
.moon {
  position: absolute;

  top: 50%;
  left: 50%;

  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;

  border-radius: 50%;

  animation-timing-function: linear;
  animation-iteration-count: infinite;

  transform-style: preserve-3d;
}

.sun {
  width: $sun;
  height: $sun;

  margin-top: -$sun / 2;
  margin-left: -$sun / 2;

  background-image: url("/images/sun.png");

  transform: rotateX(-90deg);
}

.moon {
  left: -$moon / 2;

  width: $moon;
  height: $moon;

  background: $moon-bg-color;

  animation-name: animate--planet;
  animation-duration: revolution($moon-revolution-days);
}

$planets: (
  "mercury": (
    left: -$mercury-planet / 2,
    width: $mercury-planet,
    height: $mercury-planet,
    backgroundImage: url("/images/mercury.jpg"),
    animationName: (animate--planet, shadow-mercury),
    animationDuration: revolution($mercury-revolution-days),
  ),
  "venus": (
    left: -$venus-planet / 2,
    width: $venus-planet,
    height: $venus-planet,
    backgroundImage: url("/images/venus.jpg"),
    animationName: (animate--planet, shadow-venus),
    animationDuration: revolution($venus-revolution-days),
  ),
  "earth": (
    left: -$earth-planet / 2,
    width: $earth-planet,
    height: $earth-planet,
    backgroundImage: url("/images/earth.jpg"),
    animationName: (animate--planet, shadow-earth),
    animationDuration: revolution($earth-revolution-days),
  ),
  "mars": (
    left: -$mars-planet / 2,
    width: $mars-planet,
    height: $mars-planet,
    backgroundImage: url("/images/mars.jpg"),
    animationName: (animate--planet, shadow-mars),
    animationDuration: revolution($mars-revolution-days),
  ),
  "jupiter": (
    left: -$jupiter-planet / 2,
    width: $jupiter-planet,
    height: $jupiter-planet,
    backgroundImage: url("/images/jupiter.jpg"),
    animationName: (animate--planet, shadow-jupiter),
    animationDuration: revolution($jupiter-revolution-days),
  ),
  "saturn": (
    left: -$saturn-planet / 2,
    width: $saturn-planet,
    height: $saturn-planet,
    backgroundImage: url("/images/saturn.jpg"),
    animationName: (animate--planet, shadow-saturn),
    animationDuration: revolution($saturn-revolution-days),
  ),
  "uranus": (
    left: -$uranus-planet / 2,
    width: $uranus-planet,
    height: $uranus-planet,
    backgroundImage: url("/images/uranus.jpg"),
    animationName: (animate--planet, shadow-uranus),
    animationDuration: revolution($uranus-revolution-days),
  ),
  "neptune": (
    left: -$neptune-planet / 2,
    width: $neptune-planet,
    height: $neptune-planet,
    backgroundImage: url("/images/neptune.jpg"),
    animationName: (animate--planet, shadow-neptune),
    animationDuration: revolution($neptune-revolution-days),
  ),
);

@each $name, $planet in $planets {
  .planet--#{$name} {
    left: map-get($planet, left);

    width: map-get($planet, width);
    height: map-get($planet, height);

    background-image: map-get($planet, backgroundImage);

    animation-name: map-get($planet, animationName);
    animation-duration: map-get($planet, animationDuration);
  }
}

.planet--saturn {
  &::before,
  &::after {
    position: absolute;

    content: "";

    border-radius: 50%;

    transform: rotateX(90deg);
  }

  &::before {
    top: -20px;
    left: -20px;

    width: 90px;
    height: 90px;

    border: 5px solid rgba($saturn-ring-bg-color, .5);
  }

  &::after {
    top: -13px;
    left: -13px;

    width: 65px;
    height: 65px;

    border: 10px solid rgba($saturn-ring-bg-color, .7);
  }
}

/**
 * Animations.
*/

@keyframes animate--orbit {
  0% {
    transform: rotateZ(0deg);
  }

  100% {
    transform: rotateZ(-360deg);
  }
}

@keyframes animate--suborbit {
  0% {
    transform: rotateX(90deg) rotateZ(0deg);
  }

  100% {
    transform: rotateX(90deg) rotateZ(-360deg);
  }
}

@keyframes animate--planet {
  0% {
    transform: rotateX(-90deg) rotateY(360deg) rotateZ(0deg);
  }

  100% {
    transform: rotateX(-90deg) rotateY(0deg) rotateZ(0deg);
  }
}

// Shadow Animations
$shadows: (
  "mercury": (
    0%: inset 4px 0 5px $shadow-color,
    25%: inset 22px -10px 13px $shadow-color,
    25.001%: inset -22px -10px 13px $shadow-color,
    50%: inset -12px 0 5px $shadow-color,
    75%: inset -2px 3px 2px $shadow-color,
    100%: inset 4px 0 5px $shadow-color,
  ),
  "venus": (
    0%: inset 4px 0 5px $shadow-color,
    25%: inset 22px -10px 13px $shadow-color,
    25.001%: inset -22px -10px 13px $shadow-color,
    50%: inset -12px 0 5px $shadow-color,
    75%: inset -2px 3px 2px $shadow-color,
    100%: inset 4px 0 5px $shadow-color,
  ),
  "earth": (
    0%: inset 8px 0 5px $shadow-color,
    25%: inset 45px -20px 25px $shadow-color,
    25.001%: inset -45px -20px 25px $shadow-color,
    50%: inset -8px 0 5px $shadow-color,
    75%: inset -2px 3px 2px $shadow-color,
    100%: inset 8px 0 5px $shadow-color,
  ),
  "mars": (
    0%: inset 4px 0 5px $shadow-color,
    25%: inset 22px -10px 13px $shadow-color,
    25.001%: inset -22px -10px 13px $shadow-color,
    50%: inset -12px 0 5px $shadow-color,
    75%: inset -2px 3px 2px $shadow-color,
    100%: inset 4px 0 5px $shadow-color,
  ),
  "jupiter": (
    0%: inset 16px 0 5px $shadow-color,
    25%: inset 80px -30px 50px $shadow-color,
    25.001%: inset -80px -30px 50px $shadow-color,
    50%: inset -16px 0 5px $shadow-color,
    75%: inset -2px 3px 2px $shadow-color,
    100%: inset 16px 0 5px $shadow-color,
  ),
  "saturn": (
    0%: inset 16px 0 5px $shadow-color,
    25%: inset 80px -30px 50px $shadow-color,
    25.001%: inset -80px -30px 50px $shadow-color,
    50%: inset -16px 0 5px $shadow-color,
    75%: inset -2px 3px 2px $shadow-color,
    100%: inset 16px 0 5px $shadow-color,
  ),
  "uranus": (
    0%: inset 8px 0 5px $shadow-color,
    25%: inset 40px -15px 40px $shadow-color,
    25.001%: inset -40px -15px 40px $shadow-color,
    50%: inset -8px 0 5px $shadow-color,
    75%: inset 0 0 2px $shadow-color,
    100%: inset 8px 0 5px $shadow-color,
  ),
  "neptune": (
    0%: inset 12px 0 5px $shadow-color,
    25%: inset 50px -15px 40px $shadow-color,
    25.001%: inset -50px -15px 40px $shadow-color,
    50%: inset -12px 0 5px $shadow-color,
    75%: inset 0 0 2px $shadow-color,
    100%: inset 12px 0 5px $shadow-color,
  ),
);

@each $name, $shadow in $shadows {
  @keyframes shadow-#{$name} {
    0% {
      box-shadow: map-get($shadow, 0%);
    }

    25% {
      box-shadow: map-get($shadow, 25%);

    }

    25.001% {
      box-shadow: map-get($shadow, 25.001%);
    }

    50% {
      box-shadow: map-get($shadow, 50%);
    }

    75% {
      box-shadow: map-get($shadow, 75%);
    }

    100% {
      box-shadow: map-get($shadow, 100%);
    }
  }
}

You have just witnessed the creation of a galaxy. Or at least a part of it. We created our own 3D solar system from scratch using HTML. The planets can travel around the Sun and rotate fueled only by CSS animations.

And just like that, with only HTML and CSS, the Universe is closer than ever. We can leave space tourism for the generations to come.

Who says that developers can't be astronauts?

Back