HapplyUI

Components

Fade Scroll

A scroll container that swaps the native scrollbar for soft gradient edge fades. The fade only shows on edges that still have more content to reveal, and the component works in either vertical or horizontal orientation.


Installation

bunx @happlyui/cli@latest add fade-scroll

Dependencies

Registry dependencies

These are automatically installed when you add this component.

  • happly-ui-utils

Usage

import * as FadeScroll from "@/components/ui/fade-scroll"

<FadeScroll.Root className="h-64">
  {/* scrollable content */}
</FadeScroll.Root>

Examples

Vertical

The default orientation. The top fade appears once you scroll past the first line; the bottom fade disappears at the end.

Edge fades only appear on the side that still has more content to reveal — once you reach the top or bottom, that fade disappears.

The native scrollbar is hidden, so the fades carry the entire affordance for "there is more here". This works equally well in a vertical or horizontal arrangement.

Resize observers watch both the container and its direct children, so dynamically growing content (e.g. expanding validation errors, async-loaded items) keeps the fades accurate.

Use the `fadeSize` prop to make the fade softer or harder. Use `className` to style the container itself — width, height, padding, gap, etc.

Because the component composes around `overflow-auto`, you can drop any layout inside it: a flex column of cards, a grid, a list of buttons, or just plain prose like this.

Try scrolling. The top fade will appear once you move past the first line, and the bottom fade will disappear once you reach the last paragraph.

This last paragraph exists purely so that there is enough content for the bottom fade to be visible at rest — without overflow there would be nothing to fade.

<FadeScroll.Root className="h-64 max-w-md space-y-3">
  {paragraphs.map((p, i) => <p key={i}>{p}</p>)}
</FadeScroll.Root>

Horizontal

Set orientation='horizontal' and lay out children with flex/gap. Left and right fades replace the horizontal scrollbar.

Card 1
Card 2
Card 3
Card 4
Card 5
Card 6
Card 7
Card 8
Card 9
Card 10
Card 11
Card 12
<FadeScroll.Root orientation="horizontal" className="flex max-w-lg gap-3">
  {cards.map((c) => <Card key={c.id} {...c} />)}
</FadeScroll.Root>

Vertical List

A scrollable list inside a bordered card. Children's natural size is preserved — the [&>*]:shrink-0 rule prevents flex/grid from compressing items below their content size.

  • List item #1
  • List item #2
  • List item #3
  • List item #4
  • List item #5
  • List item #6
  • List item #7
  • List item #8
  • List item #9
  • List item #10
  • List item #11
  • List item #12
  • List item #13
  • List item #14
  • List item #15
  • List item #16
  • List item #17
  • List item #18
  • List item #19
  • List item #20
  • List item #21
  • List item #22
  • List item #23
  • List item #24
<FadeScroll.Root className="h-72 w-72 rounded-lg border">
  <ul>
    {items.map((item) => <li key={item.id}>{item.label}</li>)}
  </ul>
</FadeScroll.Root>

Custom Fade Size

Override the fade distance with the fadeSize prop (in pixels). Smaller values give a sharper edge; larger values give a softer, more dramatic fade.

fadeSize=8 (subtle)

Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10

fadeSize=64 (dramatic)

Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
<FadeScroll.Root orientation="horizontal" fadeSize={64} className="flex max-w-lg gap-3">
  {items}
</FadeScroll.Root>

No Overflow

When the content fits inside the container, no fades render — the component is visually inert until scrolling becomes possible.

When content fits, no fades appear at all — the component is invisible in the layout until scroll is actually possible.

<FadeScroll.Root className="h-64 max-w-md">
  <p>Short content that doesn't overflow.</p>
</FadeScroll.Root>

API Reference

FadeScroll.Root

Scroll container with hidden native scrollbar and gradient mask fades on edges with overflowing content. Forwards a ref to the underlying div and accepts all standard div props (className, style, onScroll, etc.).

PropTypeDefaultDescription
orientation'vertical' | 'horizontal''vertical'Scroll axis. 'vertical' uses overflow-y-auto with top/bottom fades; 'horizontal' uses overflow-x-auto with left/right fades.
fadeSizenumber24Length of the gradient fade in pixels. Larger values produce a softer fade; smaller values produce a sharper edge.
classNamestring-Additional class names. Use this to set the container's height (vertical) or width (horizontal), padding, layout (flex/grid), and any other styling. Sizing is intentionally not opinionated.
styleReact.CSSProperties-Inline styles. Merged with the mask styles applied internally — your styles take precedence on conflicting keys.
onScroll(event: React.UIEvent<HTMLDivElement>) => void-Forwarded to the inner scroll div. Called after the internal scroll-state update so fades stay accurate.

Previous
Accordion