Entry 03

Styles that apply only when a scroller overflows

Use it on any row that may or may not fit, such as tabs, a toolbar or a table wrapper. It fades the edges only when there is something to scroll to. It replaces a ResizeObserver that compares scrollWidth with clientWidth.

Open
At full width both rows fit and nothing is faded. Make the frame narrower until they overflow. The first row fades at both edges. The second fades only the side that still has content: sharp at the start, then fading there too once you scroll.
Code CSS · 2 blocks · 47 lines
@keyframes overflowing {
  from, to { --fade: 2rem; }
}

.scroller {
  --fade: 0px;
  overflow-x: auto;
  animation: overflowing linear;
  animation-timeline: scroll(self inline);
  mask-image: linear-gradient(
    to right,
    transparent,
    #000 var(--fade),
    #000 calc(100% - var(--fade)),
    transparent
  );
}

To fade only the side that still has content, register the two lengths and give the keyframes different ends:

@property --fade-start {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

@property --fade-end {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

@keyframes edges {
  from { --fade-start: 0px; --fade-end: 2rem; }
  10%, 90% { --fade-start: 2rem; --fade-end: 2rem; }
  to { --fade-start: 2rem; --fade-end: 0px; }
}

.scroller-edges {
  overflow-x: auto;
  animation: edges linear;
  animation-timeline: scroll(self inline);
  mask-image: linear-gradient(
    to right,
    transparent,
    #000 var(--fade-start),
    #000 calc(100% - var(--fade-end)),
    transparent
  );
}
Why it works, the rules and the credits 4 reasons · 4 rules · support · credits

Why it works

  1. A scroll timeline is inactive while its scroller has nothing to scroll. An animation on an inactive timeline has no effect, so the keyframes apply only when the content overflows.
  2. In the first block from and to hold the same value. Scroll position changes nothing and the animation is an on/off switch.
  3. The keyframes set a custom property, and custom properties inherit. The scroller and everything inside it can read the result.
  4. A mask fades the content itself, so it works over any background. An overlay gradient has to match the color behind it.

Rules

  1. Write animation-timeline after animation. The shorthand resets the timeline.
  2. Declare the default outside the keyframes, as --fade: 0px does here.
  3. Any value can go in the keyframes, including keywords. Stripe flips its navigation to row-reverse this way when a translation is too long to fit.
  4. Write no fallback. Firefox has no scroll timelines, so the row scrolls there without the fade.

Support

animation-timeline in Chrome 115 and Safari 26. Firefox has it in Nightly only.

Credits

Borrowed from: Stripe, in the navigation on stripe.com · Bramus Van Damme, Detect if an element can scroll or not with only CSS

Docs: MDN: scroll()