Scroll Fade

A scroll container whose edges fade only where there is more to scroll, animated as you scroll.

Changelog

16 releases
  • 2.4.0Scroll fadeNew
  • 2.3.2Tabs keyboard navigationSep 28
  • 2.3.1Highlight follows scrollingSep 21
  • 2.3.0Base UI drawerNew
  • 2.2.4Faster tooltip warm-upSep 7
  • 2.2.3Reduced motion everywhereAug 31
  • 2.2.2React Aria selectAug 24
  • 2.2.1Tempo providerAug 17
  • 2.2.0AccordionNew
  • 2.1.0Motion 14Aug 3
  • 2.0.3Sliding number decimalsJul 27
  • 2.0.2Dialog origin from triggerJul 20
  • 2.0.1Smaller bundleJul 13
  • 2.0.0Drop-in primitivesNew
  • 1.9.4Icon loop delayJun 29
  • 1.9.3Ripple on keyboard pressJun 22
"use client"

import { ScrollFade } from "@animate-ui/react/primitives/effects/scroll-fade"

const RELEASES = [
  { version: "2.4.0", date: "Oct 2", title: "Scroll fade", tag: "New" },
  { version: "2.3.2", date: "Sep 28", title: "Tabs keyboard navigation" },
  { version: "2.3.1", date: "Sep 21", title: "Highlight follows scrolling" },
  { version: "2.3.0", date: "Sep 14", title: "Base UI drawer", tag: "New" },
  { version: "2.2.4", date: "Sep 7", title: "Faster tooltip warm-up" },
  { version: "2.2.3", date: "Aug 31", title: "Reduced motion everywhere" },
  { version: "2.2.2", date: "Aug 24", title: "React Aria select" },
  { version: "2.2.1", date: "Aug 17", title: "Tempo provider" },
  { version: "2.2.0", date: "Aug 10", title: "Accordion", tag: "New" },
  { version: "2.1.0", date: "Aug 3", title: "Motion 14" },
  { version: "2.0.3", date: "Jul 27", title: "Sliding number decimals" },
  { version: "2.0.2", date: "Jul 20", title: "Dialog origin from trigger" },
  { version: "2.0.1", date: "Jul 13", title: "Smaller bundle" },
  { version: "2.0.0", date: "Jul 6", title: "Drop-in primitives", tag: "New" },
  { version: "1.9.4", date: "Jun 29", title: "Icon loop delay" },
  { version: "1.9.3", date: "Jun 22", title: "Ripple on keyboard press" },
]

export default function ScrollFadeDemo({
  size = 40,
  shrink = 0.03,
  blur = 4,
}: {
  size?: number
  shrink?: number
  blur?: number
}) {
  return (
    <div className="w-full max-w-xs overflow-hidden rounded-xl border bg-card">
      <div className="flex items-center justify-between border-b px-4 py-3">
        <h3 className="text-sm font-medium">Changelog</h3>
        <span className="text-xs text-muted-foreground">
          {RELEASES.length} releases
        </span>
      </div>
      <ScrollFade size={size} shrink={shrink} blur={blur} className="h-64">
        <ul className="flex flex-col p-2">
          {RELEASES.map((release) => (
            <li
              key={release.version}
              className="flex items-center gap-3 rounded-md px-2 py-1"
            >
              <span className="w-12 shrink-0 font-mono text-xs text-muted-foreground">
                {release.version}
              </span>
              <span className="flex-1 truncate text-sm">{release.title}</span>
              {release.tag ? (
                <span className="rounded-full bg-primary px-1.5 text-[9px] leading-4 font-medium text-primary-foreground">
                  {release.tag}
                </span>
              ) : (
                <span className="text-xs text-muted-foreground">
                  {release.date}
                </span>
              )}
            </li>
          ))}
        </ul>
      </ScrollFade>
    </div>
  )
}

Installation

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Usage

import { ScrollFade } from '@animate-ui/react/primitives/effects/scroll-fade';

export function Changelog() {
  return (
    <ScrollFade className="h-64">
      <ul>{/* … */}</ul>
    </ScrollFade>
  );
}

orientation="horizontal" fades the left and right edges, "both" all four. With asChild, the child element becomes the scroll container.

To fade a scroll element you don't render yourself (a library's viewport), pass its ref to useScrollFade:

import { useRef } from 'react';
import { ScrollArea } from 'radix-ui';
import { useScrollFade } from '@animate-ui/react/primitives/effects/scroll-fade';

function Sidebar() {
  const viewportRef = useRef<HTMLDivElement>(null);
  useScrollFade(viewportRef, { size: 32 });
  return <ScrollArea.Viewport ref={viewportRef}>{/* … */}</ScrollArea.Viewport>;
}

Animation

  • An edge fades only when there is content past it: none at the edge you're at, none at all when nothing overflows. The fade follows the scroll position directly, with no lag: over the first half of size scrolled away from an edge (20px by default), that edge goes from sharp to faded, and back as you return. The fade keeps its depth (size); only how much the edge clears changes.
  • On page load, the fade is already in place: nothing animates in.
  • The items shrink and blur into the fade with it: from 1.75 times size away from a faded edge, each one eases toward 97% (shrink, 0.03) and a 4px blur (blur) as its center nears the edge, following the scroll the same way. Their places are measured when the content changes and once as a scroll starts, never every frame, and they're written once per frame. An item with its own filter keeps it and isn't blurred. The items are the children of the first element holding several (a list's items, through its ul), and a group among them is opened up: a ul, ol, role="list" or role="group", or an element holding one (a section with its title and list), shrinks line by line, never as a whole. They're shrunk with the scale property so their own transform stays.
  • It's a mask-image, so the content itself dissolves: it works on any background, in light and dark.
  • Scrolling never re-renders React: the amounts are motion values written to CSS variables (--scroll-fade-top, --scroll-fade-bottom, --scroll-fade-left, --scroll-fade-right, from 0 to 1).
  • When the content changes size without a scroll (items added or removed), the edges ease to their new amount (200ms, transition).
  • Under reduced motion, the fade and the blur follow the scroll position the same way, and jump when the content changes size; nothing shrinks.

API Reference

ScrollFade

PropTypeDefault
asChild?
booleanfalse
orientation?
'vertical' | 'horizontal' | 'both''vertical'
size?
number40
shrink?
number0.03
blur?
number4
transition?
Transition{ duration: 0.2, ease: [0.23, 1, 0.32, 1] }
enabled?
booleantrue
...props?
React.ComponentProps<'div'>-

useScrollFade

useScrollFade(ref, options) applies the fade to the element of ref. It takes the options of ScrollFade (orientation, size, shrink, blur, transition, enabled) and returns the motion values of each edge (top, bottom, left, right). It doesn't set the overflow: the element must already scroll.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page