# Scroll Fade

A scroll area whose edges fade only where there's more to scroll, so the fade itself tells people which way to go. Vertical or horizontal.

**Category:** Display  
**Exports:** `ScrollFade`  
**npm dependencies:** none (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/scroll-fade.json
```
The file lands at `components/ui/scroll-fade.tsx`; import from `@/components/ui/scroll-fade`.

## API — Scroll Fade
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — |  |
| `orientation` | `"vertical" \| "horizontal"` | `"vertical"` |  |
| `size` | `number` | `40` | Fade length in px. |
| `className` | `string` | — |  |

## Example — Scroll Fade

```tsx
import { ScrollFade } from "@/components/ui/scroll-fade";

const items = ["Design review", "Ship pricing page", "Fix login bug", "Write changelog", "Plan Q4 roadmap", "Interview candidate", "Update docs", "Refactor billing", "Team retro", "Reply to support", "Record demo", "Clean up backlog"];

export default function ScrollFadeDemo({ size = 40 }: { size?: number }) {
  return (
    <ScrollFade size={size} className="h-64 w-full max-w-xs border bg-card">
      <ul className="p-2">
        {items.map((t, i) => (
          <li key={t} className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm">
            <span className="font-mono text-xs text-muted-foreground">{String(i + 1).padStart(2, "0")}</span>
            {t}
          </li>
        ))}
      </ul>
    </ScrollFade>
  );
}
```

## Source — components/ui/scroll-fade.tsx

```tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

export type ScrollFadeProps = {
  children: React.ReactNode;
  orientation?: "vertical" | "horizontal";
  /** Fade length in px. */
  size?: number;
  className?: string;
};

/** A scroll area whose edges fade out only where there's more to scroll, so the fade itself tells you which way to go. */
export function ScrollFade({ children, orientation = "vertical", size = 40, className }: ScrollFadeProps) {
  const ref = React.useRef<HTMLDivElement>(null);
  const [edges, setEdges] = React.useState({ start: 0, end: 0 });
  const v = orientation === "vertical";

  const measure = React.useCallback(() => {
    const el = ref.current;
    if (!el) return;
    const pos = v ? el.scrollTop : el.scrollLeft;
    const max = v ? el.scrollHeight - el.clientHeight : el.scrollWidth - el.clientWidth;
    // Fades grow over the first stretch of scrolling instead of snapping on.
    setEdges({ start: Math.min(1, pos / size), end: Math.min(1, Math.max(0, max - pos) / size) });
  }, [v, size]);

  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    if (el.firstElementChild) ro.observe(el.firstElementChild);
    return () => ro.disconnect();
  }, [measure]);

  const dir = v ? "to bottom" : "to right";
  const mask = `linear-gradient(${dir}, rgb(0 0 0 / ${1 - edges.start}) 0, #000 ${size}px, #000 calc(100% - ${size}px), rgb(0 0 0 / ${1 - edges.end}) 100%)`;

  return (
    <div
      ref={ref}
      tabIndex={0}
      onScroll={measure}
      className={cn("rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring", v ? "overflow-y-auto" : "overflow-x-auto", className)}
      style={{ maskImage: mask, WebkitMaskImage: mask }}
    >
      <div className={v ? undefined : "w-max"}>{children}</div>
    </div>
  );
}
```