Skip to content

Scroll Fades

A list cut off by a hard edge looks like it ends there. A scroll fade dissolves the last visible row into the background, so the reader knows there is more without looking for a scrollbar.

Wrong
  • Review pull request #482
  • Update onboarding copy
  • Fix focus ring on the date picker
  • Write release notes for 2.4
  • Reply to the design review thread
  • Migrate icons to the new set
  • Check contrast on the dark theme
  • Prepare the roadmap for Q4
  • Book the offsite venue
  • Archive the old marketing site
Hard edge
Right
  • Review pull request #482
  • Update onboarding copy
  • Fix focus ring on the date picker
  • Write release notes for 2.4
  • Reply to the design review thread
  • Migrate icons to the new set
  • Check contrast on the dark theme
  • Prepare the roadmap for Q4
  • Book the offsite venue
  • Archive the old marketing site
40px fade

The fade is a mask-image gradient. Where the gradient is transparent the content is hidden, and where it is black the content shows. Because it is a mask and not an overlay, it works on any background and never blocks a click.

Follow the scroll

A static mask fades the first row while you are at the top, and the last row when there is nothing below it. That last one lies. The final item looks like something is still hiding under it.

Fade an edge only when there is more to scroll past it.

Wrong
  • Review pull request #482
  • Update onboarding copy
  • Fix focus ring on the date picker
  • Write release notes for 2.4
  • Reply to the design review thread
  • Migrate icons to the new set
  • Check contrast on the dark theme
  • Prepare the roadmap for Q4
  • Book the offsite venue
  • Archive the old marketing site
Always faded
Right
  • Review pull request #482
  • Update onboarding copy
  • Fix focus ring on the date picker
  • Write release notes for 2.4
  • Reply to the design review thread
  • Migrate icons to the new set
  • Check contrast on the dark theme
  • Prepare the roadmap for Q4
  • Book the offsite venue
  • Archive the old marketing site
Follows the scroll

Scroll both lists to the top and the bottom. The right list grows each fade over the first 40px of scroll in that direction, so it follows the scroll instead of popping in.

With scroll-driven animations

This needs no JavaScript. animation-timeline: scroll(self) ties an animation to the element's own scroll position, and animation-range limits it to the first and last 40px. The two fade sizes are custom properties registered with @property, so they can animate.

The same rule works sideways with scroll(self x).

Wrong
Right

Usage

app.tsx
<!-- Static fade, 40px at both ends -->
<div
  class="h-44 overflow-y-auto mask-[linear-gradient(to_bottom,transparent,black_40px,black_calc(100%-40px),transparent)]"
>
  ...
</div>
@property --fade-start {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

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

@keyframes fade-start {
  from { --fade-start: 0px; }
  to { --fade-start: 40px; }
}

@keyframes fade-end {
  from { --fade-end: 40px; }
  to { --fade-end: 0px; }
}

.list {
  overflow-y: auto;
  --fade-start: 40px;
  --fade-end: 40px;
  mask-image: linear-gradient(
    to bottom,
    transparent,
    black var(--fade-start),
    black calc(100% - var(--fade-end)),
    transparent
  );
}

@supports (animation-timeline: scroll()) {
  .list {
    --fade-start: 0px;
    --fade-end: 0px;
    animation: fade-start linear both, fade-end linear both;
    animation-timeline: scroll(self);
    animation-range: 0 40px, calc(100% - 40px) 100%;
  }
}

Scroll-driven animations work in Chrome and Edge 115+ and Safari 26+. As of October 2026, Firefox only has them in Nightly, so the @supports block keeps the static fade there. If you need the scroll-linked fade in Firefox too, set the two properties from a scroll listener, as the demos on this page do.

Resources

Search concepts

Search for a command to run...