dqnamoexperiments / scroll-fade-list

Scroll Fade List

A scrollable list with gradient fade edges to show that there is more content above or below.

  1. South Africa
  2. Canada
  3. Brazil
  4. Japan
  5. Germany
  6. Paraguay
  7. Netherlands
  8. Morocco
  9. Ivory Coast
  10. Norway
  11. France
  12. Sweden
  13. Mexico
  14. Ecuador
  15. England
  16. DR Congo
  17. Belgium
  18. Senegal
  19. United States
  20. Bosnia and Herzegovina
  21. Spain
  22. Austria
  23. Portugal
  24. Croatia
  25. Switzerland
  26. Algeria
  27. Australia
  28. Egypt
  29. Argentina
  30. Cabo Verde
  31. Colombia
  32. Ghana
"use client";import { type ReactNode, useEffect, useRef } from "react";type ScrollFadeListProps<TItem> = {  getKey: (item: TItem) => string;  items: readonly TItem[];  renderItem: (item: TItem) => ReactNode;  className?: string;  maxFadeHeight?: number;  scrollClassName?: string;};export function ScrollFadeList<TItem>({  className = "relative overflow-hidden rounded-xl border bg-white [--scroll-fade-list-bg:white] [--scrollbar-gutter-width:10px]",  getKey,  items,  maxFadeHeight = 76,  renderItem,  scrollClassName = "h-80 overflow-y-auto overscroll-contain [scrollbar-gutter:stable]",}: ScrollFadeListProps<TItem>) {  const frameRef = useRef<HTMLDivElement>(null);  const scrollRef = useRef<HTMLDivElement>(null);  useEffect(() => {    const frameElement = frameRef.current;    const scrollElement = scrollRef.current;    let animationFrame = 0;    if (!frameElement || !scrollElement) {      return;    }    const updateFadeHeights = () => {      const maxScrollTop = Math.max(        0,        scrollElement.scrollHeight - scrollElement.clientHeight,      );      const distanceFromTop = scrollElement.scrollTop;      const distanceFromBottom = maxScrollTop - scrollElement.scrollTop;      frameElement.style.setProperty(        "--top-fade-height",        `${Math.min(maxFadeHeight, Math.max(0, distanceFromTop))}px`,      );      frameElement.style.setProperty(        "--bottom-fade-height",        `${Math.min(maxFadeHeight, Math.max(0, distanceFromBottom))}px`,      );    };    const scheduleFadeUpdate = () => {      cancelAnimationFrame(animationFrame);      animationFrame = requestAnimationFrame(updateFadeHeights);    };    updateFadeHeights();    scrollElement.addEventListener("scroll", scheduleFadeUpdate, {      passive: true,    });    const resizeObserver = new ResizeObserver(scheduleFadeUpdate);    resizeObserver.observe(scrollElement);    if (scrollElement.firstElementChild) {      resizeObserver.observe(scrollElement.firstElementChild);    }    return () => {      cancelAnimationFrame(animationFrame);      scrollElement.removeEventListener("scroll", scheduleFadeUpdate);      resizeObserver.disconnect();    };  }, [maxFadeHeight]);  return (    <div      className={`${className} [--bottom-fade-height:0px] [--top-fade-height:0px] before:pointer-events-none before:absolute before:left-0 before:right-[var(--scrollbar-gutter-width)] before:top-0 before:h-[var(--top-fade-height)] before:bg-linear-to-b before:from-[var(--scroll-fade-list-bg)] before:to-transparent before:content-[''] after:pointer-events-none after:absolute after:bottom-0 after:left-0 after:right-[var(--scrollbar-gutter-width)] after:h-[var(--bottom-fade-height)] after:bg-linear-to-b after:from-transparent after:to-[var(--scroll-fade-list-bg)] after:content-['']`}      ref={frameRef}    >      <div className={scrollClassName} ref={scrollRef}>        <ul className="p-2">          {items.map((item) => (            <li className="rounded-md px-3 py-2" key={getKey(item)}>              {renderItem(item)}            </li>          ))}        </ul>      </div>    </div>  );}

Want to work with me?

I run a lil design engineering studio in London where we do fractional design engineering for startups.

THEINTERFACECOMPANYOF LONDON