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 ofnearest|root|self
.<axis>
can be any of the directional valuesblock|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.
-
Although this
<scroll-shadow>
web component exists and is cool. ↩ -
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. ↩