View Demo on CodePen

I got myself into a position: sticky + horizontal scrolling situation with some overflowing tables the other day. The sticking worked fine but I was hoping to add shadows as an affordance that an overflow was occurring on the element. But you know me, I didn’t want to install some JavaScript component to do the job1. I wanted to find the laziest low-code way possible. Hyped up by Bramus’s new scroll-driven-animations.style website I wondered if scroll-driven animations could do the job.

  • Project Goal: Have a shadow appear when an element is overflowing as a hint that more content is available.
  • Bonus Goal: Add a shadow to sticky column as well for visual depth.

Let’s get started. The new syntax2 for scroll-based animations recycles the well-known @keyframes animation spec. There’s one new property-value combo you need to know:

.my-element {
  animation-timeline: scroll( <scroller> <axis> )
}

The new animation-timeline property accepts a scroll() function which takes two arguments.

  • <scroller> points to the element with the scrollbar and accepts values of nearest|root|self.
  • <axis> can be any of the directional values block|inline|y|x.

Here’s an example of how I added an inset shadow to the inside of overflow container:

.container-overflow {
  /* overflow */
  overflow-x: auto;
  width: 50%;

  /* shadow */
  animation: scroll-shadow-inset linear;
  animation-timeline: scroll( self inline );
}

@keyframes scroll-shadow-inset {
  /* start with inset shadow on right */
  from {
    box-shadow: inset -10px -10px 15px 0px rgb(0 0 0 / 0.3);
  }

  /* end with inset shadow on left */
  to {
    box-shadow: inset 10px -10px 15px 0px rgb(0 0 0 / 0.3);
  }
}

For the sticky column, I had to do something slightly different and use an regular box-shadow instead of the inset box-shadow. Also, because the sticky elements aren’t scrolling, I had to use the nearest keyword for my <scroller> value, which points to the overflow container.

.row > .cell:first-child {
    /* sticky */
    position: sticky;
    left: 0;
    background: Canvas;
    border-right: 1px solid #ddd;

    /* shadow */
    animation: scroll-shadow-sticky-elements linear;
    animation-timeline: scroll(nearest inline);
}

@keyframes scroll-shadow-sticky-elements {
  /* start with no shadow */
  from {
    box-shadow: none;
  }

  /* end with shadow pushing out right */
  to {
    box-shadow: 10px 10px 15px 0px rgb(0 0 0 / 0.3);
  }
}

🎉 We did it! The coolest part about this solution is that the scroll() animation only shows a shadow when there’s overflowing content that creates a scrollbar. There’s a slight bonus UX happening too where the shadow gets more prominent the “deeper” you scroll. All with just a few lines of code.

I’m happy with how this turned out. Now that I’ve tasted the future of scroll-based animations, I’m excited to (ab)use this more. Part of the reason I avoid doing more JavaScript-based scroll animations is because of all the JS that’s involved, it’s a lot of effort and technical debt to create even a subtle scroll animation. But with a CSS one-liner and some @keyframes behind the scenes… well, now the game has changed. I don’t have to futz with Intersection Observer or use the Sticky Sentinal pattern; it’s like a native isOverflowing check right there in CSS.

Browser support is pretty limited (Chrome 115+ at time of writing) but there is a polyfill for scroll-timeline if you get yourself into a stickier situation. For me and my projects, I’m going to treat animation-timeline: scroll() as a nice progressive enhancement.

  1. Although this <scroll-shadow> web component exists and is cool.

  2.  The old syntax is uses a @scroll-timeline keyframe system. There’s a handful of articles out there with that syntax. If you see that, close the browser.