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.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | — | 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 |
children | ReactNode | — | Content placed directly in the scroll container when items is not used |
maxFadeHeight | number | 76 | Maximum height (px) of the top and bottom fades; actual height = min(remaining scroll distance, this value) |
height | number | string | 320 | Height of the scroll area; numbers are px, or pass any CSS length string |
autoScroll | boolean | false | Scroll down automatically and loop back to the top at the end; pauses on hover, keyboard focus and touch, and is ignored under reduced motion |
gutter | number | 10 | Width (px) reserved on the right for the scrollbar; the fades do not cover it |
label | string | "List" | Accessible name of the scroll area (aria-label of role="region") |
listClassName | string | — | Passed to the <ul> (items mode) or the content wrapper (children mode) |
className | string | — | Passed to the outermost container |
CSS variables
| Field | Type | Default | Description |
|---|---|---|---|
--wb-sfl-bg | color | #ffffff (dark: #0a0a0a) | Solid end of the fades; must match the surface behind the list |
--wb-sfl-auto-speed | number | 24 | Auto-scroll speed (px per second), re-read by the component every second |
--top-fade-height | length | — | Top fade height written by the component while scrolling; do not set by hand |
--bottom-fade-height | length | — | Bottom fade height written by the component while scrolling; do not set by hand |
How it works
- Scroll events are listened to with
passive: trueand throttled withrequestAnimationFrame. Each frame measuresscrollTop(distance to the top) andscrollHeight − clientHeight − scrollTop(distance to the bottom), clamps each between 0 andmaxFadeHeight, and writes them straight into CSS variables withstyle.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
ResizeObserverwatches 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/::afterwithpointer-events: none, so they never block clicks, and they stop short of thegutteron the right so the scrollbar stays visible. The scroll container usesscrollbar-gutter: stableandoverscroll-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"withlabelas itsaria-label, andtabIndex=0lets 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>withrole="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
autoScrollis disabled