CSS Tip: Performant Box-Shadow Transitions

Joshua Bemenderfer

Have you ever been using a Material Design web app and thought “this just feels slow”? It might be because, well, it was. Material Design relies heavily on shadows to indicate depth and relationships. As AirBnB discovered, box shadows are slow. To make matters worse, animating shadow blur to make an element feel like it’s moving forward and backward is a design pattern seen all over the place. Shadows cause a repaint on every frame they’re changed, so shadow transitions are incredibly slow.

Perhaps it’s not too bad on a decently-powered computer, but you can often feel it on a mobile device. Let’s take a look at an alternative, higher-performance method to create a nearly-equivalent effect.

This method works best if you start with a subtle shadow. It may not be a fit for all use-cases.

The Method

What we’re going to do is create the initial box-shadow on the element as normal, then create a pseudo-element that has the expanded shadow. The pseudo-element will be hidden with opacity: 0;. To transition to the expanded shadow, we’ll just animate the opacity of the pseudo-element.

Note, this is not a perfect replacement. The original shadow is still present, so the inner sides of the shadow are a little darker than you might expect. That’s why this method works best when you go from a really subtle shadow to a much thicker one.

The code

/* The old, slow way. */
.slow-transition {
  box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.3);
  transition: box-shadow 500ms;
}

.slow-transition:hover {
  box-shadow: 0 10px 50px 0 rgba(0, 0, 0, 0.5);
}

/* The fast, new way! */
.fast-transition {
  position: relative; /* For positioning the pseudo-element */
  box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.3);
}

.fast-transition::before {
  /* Position the pseudo-element. */
  content: ' ';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;

  /* Create the box shadow at expanded size. */
  box-shadow: 0 10px 50px 0 rgba(0, 0, 0, 0.5);

  /* Hidden by default. */
  opacity: 0;
  transition: opacity 500ms;
}

.fast-transition:hover::before {
  /* Show the pseudo-element on hover. */
  opacity: 1;
}

The result and live demo

Here’s a codepen you can play around with.

Normal Box Shadow Transition (Slower)

Pseudo-Element Opacity Transition (Faster)

Performance Details

Okay, so I’ve told you one is faster, and one is slower, right? But why?

The key to the performance difference is that transitioning box-shadow requires a repaint and recomposite on every frame, while transitioning opacity only requires recompositing (in most browsers).

Here are the profiling results from my browser:

Transitioning box-shadow:

  • Paint Time: ~6ms (varies)
  • Number of paints: 1 per frame.

Transitioning Pseudo-Element Opacity:

  • Paint Time: ~1.5ms
  • Number of Paints: 2 total.

Credits:

  Tweet It

🕵 Search Results

🔎 Searching...