Layout Composition

Scroll Fade List

A React scrolling list whose top and bottom fades grow and shrink with the remaining scroll distance, disappearing entirely at either end, with optional auto-scroll that pauses on hover, focus and touch.

When a long list sits in a fixed-height box, edge fades hint that there is more to see — but a top fade that lingers at the very top just leaves the first row half-washed-out. In this component the fade height is the remaining scroll distance itself (capped at maxFadeHeight): the closer you get to the top, the thinner the top fade, until it disappears; the bottom works the same way. Use items + renderItem for a semantic <ul>, or pass children directly; for a ticker-style notice board, turn on autoScroll.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add scroll-fade-list.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

76
320
24
Active edited usageDefault props
<ScrollFadeList/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/scroll-fade-list.en.json

Or, once registries are configured in components.json, install it as @webberui/scroll-fade-list.

Usage

import { ScrollFadeList } from "@/components/ui/scroll-fade-list";

const areas = [
  { name: "Zhongzheng, Taipei", zip: "100" },
  { name: "Xitun, Taichung", zip: "407" },
  { name: "Lingya, Kaohsiung", zip: "802" },
];

<ScrollFadeList
  items={areas}
  getKey={(a) => a.zip}
  height={280}
  label="Delivery areas"
  renderItem={(a) => (
    <div className="flex justify-between px-3 py-2 text-sm">
      <span>{a.name}</span>
      <span className="font-mono tabular-nums">{a.zip}</span>
    </div>
  )}
/>

On a non-white surface, override --wb-sfl-bg so the solid end of the fades matches the background:

<ScrollFadeList
  className="[--wb-sfl-bg:#fafafa] dark:[--wb-sfl-bg:#171717]"
  autoScroll
>
  {/* any content */}
</ScrollFadeList>

Props

PropTypeDefaultDescription
itemsT[]—List data; used together with renderItem and getKey. When provided, renders a <ul>/<li> structure; otherwise children are rendered
renderItem(item: T, index: number) => ReactNode—Render function for each item; falls back to String(item)
getKey(item: T, index: number) => Key—Key for each item; falls back to the index
childrenReactNode—Content placed directly in the scroll container when items is not used
maxFadeHeightnumber76Maximum height (px) of the top and bottom fades; actual height = min(remaining scroll distance, this value)
heightnumber | string320Height of the scroll area; numbers are px, or pass any CSS length string
autoScrollbooleanfalseScroll down automatically and loop back to the top at the end; pauses on hover, keyboard focus and touch, and is ignored under reduced motion
gutternumber10Width (px) reserved on the right for the scrollbar; the fades do not cover it
labelstring"List"Accessible name of the scroll area (aria-label of role="region")
listClassNamestring—Passed to the <ul> (items mode) or the content wrapper (children mode)
classNamestring—Passed to the outermost container

CSS variables

FieldTypeDefaultDescription
--wb-sfl-bgcolor#ffffff (dark: #0a0a0a)Solid end of the fades; must match the surface behind the list
--wb-sfl-auto-speednumber24Auto-scroll speed (px per second), re-read by the component every second
--top-fade-heightlength—Top fade height written by the component while scrolling; do not set by hand
--bottom-fade-heightlength—Bottom fade height written by the component while scrolling; do not set by hand

How it works

  • Scroll events are listened to with passive: true and throttled with requestAnimationFrame. Each frame measures scrollTop (distance to the top) and scrollHeight − clientHeight − scrollTop (distance to the bottom), clamps each between 0 and maxFadeHeight, and writes them straight into CSS variables with style.setProperty — no React state, so the list never re-renders while scrolling.
  • The fade height deliberately has no transition: it is the remaining distance itself, and animating it would only lag behind the finger.
  • A ResizeObserver watches both the scroll container and its content, so adding or removing data, font loading or re-wrapping recomputes the fades.
  • The fades are the outer container's ::before/::after with pointer-events: none, so they never block clicks, and they stop short of the gutter on the right so the scrollbar stays visible. The scroll container uses scrollbar-gutter: stable and overscroll-behavior: contain.
  • Auto-scroll keeps its position in a float accumulator, so it moves smoothly even when advancing less than 1px per frame, and picks up from wherever the user scrolled manually. After a touch ends it waits about 1.5 seconds before resuming, letting momentum scrolling finish first.

Accessibility

  • The scroll container is role="region" with label as its aria-label, and tabIndex=0 lets keyboard users focus it and scroll with the arrow keys or Page Up/Page Down; a focus-visible ring shows when focused
  • Items mode renders a <ul>/<li> with role="list", so screen readers announce the item count
  • The fades are text-free pseudo-elements, so assistive technology does not read them and they do not intercept pointer events
  • Auto-scroll pauses on hover, when anything inside the container has focus, and during touch, so content never scrolls away while someone is reading or interacting
  • When the system "reduce motion" setting is on, the fades stay as they are (they are not an animation) and only autoScroll is disabled