Animating with transform and opacity: three CodePen case studies

Popular animations on CodePen often look complex, but many are built from a small set of performant CSS techniques. The three examples below show how transform and opacity — properties that avoid triggering layout and paint — can produce rich effects without slowing the page down.

Case study: a wizard loading animation

The first example is entirely CSS and HTML: no images or JavaScript are used to draw or animate the wizard. The moving shapes, including a triangle that spins as it rises, rely on pseudo-elements and keyframe animations.

Recording the animation in Chrome DevTools' Performance tab shows no Layout or Paint operations in the summary. To see why, open the Animations panel to locate the moving elements. Selecting the triangle, for instance, highlights its box in the DOM and shows its journey through the air.

The triangle itself is generated with the ::after pseudo-element and shaped with borders:

.triangle {
    position: absolute;
    bottom: -62px;
    left: -10px;
    width: 110px;
    height: 110px;
    border-radius: 50%;
}

.triangle::after {
    content: "";
    position: absolute;
    top: 0;
    right: -10px;
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 0 28px 48px 28px;
    border-color: transparent transparent #89beb3 transparent;
}

The animation is applied and its keyframes are visible in the Styles panel. The movement and spin are driven entirely by transform, which the browser handles without layout or paint:

animation: path_triangle 10s ease-in-out infinite;
@keyframes path_triangle {
  0% {
    transform: translateY(0);
  }
  10% {
    transform: translateY(-172px) translatex(10px) rotate(-10deg);
  }
  55% {
    transform: translateY(-172px) translatex(10px) rotate(-365deg);
  }
  58% {
    transform: translateY(-172px) translatex(10px) rotate(-365deg);
  }
  63% {
    transform: rotate(-360deg);
  }
}

Each moving part uses the same approach, so the overall effect is complex but still runs smoothly.

Case study: a pulsating circle

This second pattern is often used to draw the eye to a call-to-action or notification. Firefox DevTools shows the performance picture clearly: recording the animation sees no Recalculate Style entries in the waterfall, confirming no style recalculations are happening.

The container <div> with the class pulsating-circle marks the position but draws nothing itself:

.pulsating-circle {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translateX(-50%) translateY(-50%);
    width: 30px;
    height: 30px;
}

The visual work is split across two pseudo-elements. The ::before element paints the outer ring; its pulse-ring animation scales the ring and fades its opacity:

.pulsating-circle::before {
    content: '';
    position: relative;
    display: block;
    width: 300%;
    height: 300%;
    box-sizing: border-box;
    margin-left: -100%;
    margin-top: -100%;
    border-radius: 45px;
    background-color: #01a4e9;
    animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
}

@keyframes pulse-ring {
  0% {
    transform: scale(0.33);
  }
  80%, 100% {
    opacity: 0;
  }
}

The ::after element creates the white dot in the center, with a pulse-dot animation that uses transform: scale to grow and shrink it:

.pulsating-circle::after {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  display: block;
  width: 100%;
  height: 100%;
  background-color: white;
  border-radius: 15px;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
  animation: pulse-dot 1.25s cubic-bezier(0.455, 0.03, 0.515, 0.955) -0.4s infinite;
}

@keyframes pulse-dot {
  0% {
    transform: scale(0.8);
  }
  50% {
    transform: scale(1);
  }
  100% {
    transform: scale(0.8);
  }
}

Firefox DevTools' Animations panel offers another view into which properties are being animated, listing them alongside the timeline. This kind of small effect is easy to sprinkle throughout an application — keeping it on transform and opacity means it will not drag down overall performance.

Case study: a pure CSS 3D sphere

Visually this is the most involved of the three examples, but the underlying machinery is the same as in the previous cases. The apparent complexity comes from the number of animated elements, not from the properties being animated.

The sphere consists of multiple rotating planes and spokes. Inspecting a plane with Chrome DevTools reveals the structure: each plane and spoke is nested inside a wrapper <div>, and that wrapper uses transform: rotate3d for the rotation:

.sphere-wrapper {
  transform-style: preserve-3d;
  width: 300px;
  height: 300px;
  position: relative;
  animation: rotate3d 10s linear infinite;
}

@keyframes rotate3d {
  0% {
    transform: rotate3d(1, 1, 1, 0deg);
  }
  25% {
    transform: rotate3d(1, 1, 1, 90deg);
  }
  50% {
    transform: rotate3d(1, 1, 1, 180deg);
  }
  75% {
    transform: rotate3d(1, 1, 1, 270deg);
  }
  100% {
    transform: rotate3d(1, 1, 1, 360deg);
  }
}

The dots nested inside those elements use their own animation, scaling and translating with transform to add the pulsing effect:

.spoke-15 .dot,
.spoke-21 .dot {
  animation: pulsate 0.5s infinite 0.83333333s alternate both;
  background-color: #55ffee;
}

@-webkit-keyframes pulsate {
  0% {
    transform: rotateX(90deg) scale(0.3) translateZ(20px);
  }
  100% {
    transform: rotateX(90deg) scale(1) translateZ(0px);
  }
}

The real work here was choreography — getting the timing of the rotation and pulses to align. Once the keyframes are set, each animation individually is straightforward, and all of them travel down the performant path. A Chrome DevTools performance recording confirms that after initial load, the sphere causes no Layout or Paint work and runs smoothly.