# Scroll fade

Utilities for adding a fade effect to the edges of a scroll container.

Page: https://sui.draco.dev/docs/utils/scroll-fade

### Example: scroll-fade-demo

```tsx
// biome-ignore-all lint/a11y/noNoninteractiveTabindex: These named scroll viewports need keyboard access.
import type { ExampleProps } from "../types";
export default function ScrollFadeDemo({ locale }: ExampleProps = {}) {
  const chinese = locale === "zh-CN";
  return (
    <div className="mx-auto w-full max-w-xs overflow-hidden rounded-[14px] ring-1 ring-border">
      <section
        tabIndex={0}
        aria-label={chinese ? "可滚动条目" : "Scrollable items"}
        className="scroll-fade no-scrollbar h-72 overflow-y-auto"
      >
        <div className="flex flex-col gap-1.5 p-1.5">
          {Array.from({ length: 12 }, (_, index) => index + 1).map(
            (itemNumber) => (
              <div
                key={itemNumber}
                className="rounded-lg bg-muted px-3 py-2.5 text-sm"
              >
                {chinese ? "条目" : "Item"} {itemNumber}
              </div>
            ),
          )}
        </div>
      </section>
    </div>
  );
}
```

## Installation

Follow the [installation guide](/docs/installation). Both utilities are included when you import SUI’s global styles:

```css
@import "@workspace/ui/globals.css";
```

The utilities use the existing `shadcn/tailwind.css` import in `globals.css`; no additional stylesheet or React component is needed.

## Usage

| Class                             | Styles                                                                                                              |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `scroll-fade`                     | `mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));`  `animation-timeline: scroll(self y);`       |
| `scroll-fade-y`                   | `mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));`  `animation-timeline: scroll(self y);`       |
| `scroll-fade-x`                   | `mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline));`  `animation-timeline: scroll(self inline);` |
| `scroll-fade-t`                   | Fade mask on the top edge.  `animation-timeline: scroll(self y);`                                             |
| `scroll-fade-b`                   | Fade mask on the bottom edge.  `animation-timeline: scroll(self y);`                                          |
| `scroll-fade-l`                   | Fade mask on the left edge.  `animation-timeline: scroll(self x);`                                            |
| `scroll-fade-r`                   | Fade mask on the right edge.  `animation-timeline: scroll(self x);`                                           |
| `scroll-fade-s`                   | Fade mask on the start edge, mirrors in RTL.  `animation-timeline: scroll(self inline);`                      |
| `scroll-fade-e`                   | Fade mask on the end edge, mirrors in RTL.  `animation-timeline: scroll(self inline);`                        |
| `scroll-fade-<number>`            | `--scroll-fade-size: calc(var(--spacing) * <number>);`                                                              |
| `scroll-fade-[<value>]`           | `--scroll-fade-size: <value>;`                                                                                      |
| `scroll-fade-{t,b,s,e}-<number>`  | `--scroll-fade-{t,b,s,e}-size: calc(var(--spacing) * <number>);`                                                    |
| `scroll-fade-{t,b,s,e}-[<value>]` | `--scroll-fade-{t,b,s,e}-size: <value>;`                                                                            |
| `scroll-fade-none`                | `--scroll-fade-mask: none;`                                                                                         |

Add `scroll-fade` or `scroll-fade-y` to the scroll container, i.e. the element that has `overflow-y-auto`.

```tsx
<div className="scroll-fade overflow-y-auto">{/* ... */}</div>
```

The fade is scroll-aware and tracks the scroll position:

- At rest, the top edge is crisp and the bottom edge fades to hint at more content.
- As you scroll, a fade appears at the top and both edges stay faded mid-scroll.
- At the end, the bottom edge sharpens to show you have reached the last item.

The fade is applied with `mask-image`, so it dissolves the content itself rather than overlaying a color. The mask uses a linear fade from transparent to black, so it adapts to any background without configuration. If your scroll area sits inside a card, put the background and border on a wrapper and `scroll-fade` on the inner scroller, so the fade dissolves the content and not the card.

The [`ScrollArea`](/docs/components/scroll-area) and [`MessageScroller`](/docs/components/message-scroller) components can use `scroll-fade` on their scrollable viewport.

## No overflow

In browsers with scroll-driven animation support, content that does not overflow has no fade. You can apply `scroll-fade` without checking the scroll size. The static fallback described below still fades the selected edges.

### Example: scroll-fade-overflow

```tsx
// biome-ignore-all lint/a11y/noNoninteractiveTabindex: These named scroll viewports need keyboard access.
import type { ExampleProps } from "../types";
export default function ScrollFadeOverflow({ locale }: ExampleProps = {}) {
  const chinese = locale === "zh-CN";
  return (
    <div className="mx-auto w-full max-w-xs overflow-hidden rounded-[14px] ring-1 ring-border">
      <section
        tabIndex={0}
        aria-label={chinese ? "可滚动条目" : "Scrollable items"}
        className="scroll-fade no-scrollbar overflow-y-auto"
      >
        <div className="flex flex-col gap-1.5 p-1.5">
          {Array.from({ length: 3 }, (_, index) => index + 1).map(
            (itemNumber) => (
              <div
                key={itemNumber}
                className="rounded-lg bg-muted px-3 py-2.5 text-sm"
              >
                {chinese ? "条目" : "Item"} {itemNumber}
              </div>
            ),
          )}
        </div>
      </section>
    </div>
  );
}
```

## Horizontal scrolling

Use `scroll-fade-x` on containers that scroll horizontally, i.e. the element that has `overflow-x-auto`.

### Example: scroll-fade-horizontal

```tsx
// biome-ignore-all lint/a11y/noNoninteractiveTabindex: These named scroll viewports need keyboard access.
import type { ExampleProps } from "../types";

const tags = [
  ["Design", "设计"],
  ["Engineering", "工程"],
  ["Marketing", "市场"],
  ["Product", "产品"],
  ["Research", "研究"],
  ["Sales", "销售"],
  ["Support", "支持"],
  ["Operations", "运营"],
  ["Finance", "财务"],
  ["Legal", "法务"],
  ["People", "人力"],
  ["Security", "安全"],
];

export default function ScrollFadeHorizontal({ locale }: ExampleProps = {}) {
  const chinese = locale === "zh-CN";
  return (
    <div className="mx-auto w-full max-w-xs overflow-hidden rounded-[14px] ring-1 ring-border">
      <section
        tabIndex={0}
        aria-label={chinese ? "可滚动条目" : "Scrollable items"}
        className="scroll-fade-x no-scrollbar overflow-x-auto"
      >
        <div className="flex w-max gap-1.5 p-1.5">
          {tags.map(([tag, chineseTag]) => (
            <div
              key={chinese ? chineseTag : tag}
              className="shrink-0 rounded-lg bg-muted px-3 py-2.5 text-sm"
            >
              {chinese ? chineseTag : tag}
            </div>
          ))}
        </div>
      </section>
    </div>
  );
}
```

```tsx
<div className="flex scroll-fade-x overflow-x-auto">{/* ... */}</div>
```

The horizontal fade is direction-aware. In RTL layouts, the crisp edge and the fade follow the reading direction with no extra classes needed. `scroll-fade-<number>` and `scroll-fade-none` work the same for both axes.

## Edge fades

Use edge utilities when only one edge should track the scroll position.

### Example: scroll-fade-edge

```tsx
// biome-ignore-all lint/a11y/noNoninteractiveTabindex: These named scroll viewports need keyboard access.
import type { ExampleProps } from "../types";

const items = [
  ["Inbox triage", "收件箱整理"],
  ["Design review", "设计评审"],
  ["API contract", "API 约定"],
  ["QA pass", "质量检查"],
  ["Launch notes", "发布说明"],
  ["Metrics follow-up", "指标回顾"],
];

const tags = [
  ["Design", "设计"],
  ["Engineering", "工程"],
  ["Marketing", "市场"],
  ["Product", "产品"],
  ["Research", "研究"],
  ["Sales", "销售"],
  ["Support", "支持"],
  ["Operations", "运营"],
];

export default function ScrollFadeEdge({ locale }: ExampleProps = {}) {
  const chinese = locale === "zh-CN";
  return (
    <div className="mx-auto flex min-w-0 max-w-xs flex-col gap-6">
      <div className="flex flex-col gap-3">
        <div className="overflow-hidden rounded-[14px] ring-1 ring-border">
          <section
            tabIndex={0}
            aria-label={chinese ? "可滚动条目" : "Scrollable items"}
            className="scroll-fade-t no-scrollbar h-36 overflow-y-auto"
          >
            <ScrollFadeEdgeItems locale={locale} />
          </section>
        </div>
        <p className="text-center font-mono text-[0.9em] text-muted-foreground">
          scroll-fade-t
        </p>
      </div>
      <div className="flex flex-col gap-3">
        <div className="overflow-hidden rounded-[14px] ring-1 ring-border">
          <section
            tabIndex={0}
            aria-label={chinese ? "可滚动条目" : "Scrollable items"}
            className="scroll-fade-b no-scrollbar h-36 overflow-y-auto"
          >
            <ScrollFadeEdgeItems locale={locale} />
          </section>
        </div>
        <p className="text-center font-mono text-[0.9em] text-muted-foreground">
          scroll-fade-b
        </p>
      </div>
      <div className="flex flex-col gap-3">
        <div className="overflow-hidden rounded-[14px] ring-1 ring-border">
          <section
            tabIndex={0}
            aria-label={chinese ? "可滚动条目" : "Scrollable items"}
            className="scroll-fade-s no-scrollbar overflow-x-auto"
          >
            <ScrollFadeEdgeTags locale={locale} />
          </section>
        </div>
        <p className="text-center font-mono text-[0.9em] text-muted-foreground">
          scroll-fade-s
        </p>
      </div>
      <div className="flex flex-col gap-3">
        <div className="overflow-hidden rounded-[14px] ring-1 ring-border">
          <section
            tabIndex={0}
            aria-label={chinese ? "可滚动条目" : "Scrollable items"}
            className="scroll-fade-e no-scrollbar overflow-x-auto"
          >
            <ScrollFadeEdgeTags locale={locale} />
          </section>
        </div>
        <p className="text-center font-mono text-[0.9em] text-muted-foreground">
          scroll-fade-e
        </p>
      </div>
    </div>
  );
}

function ScrollFadeEdgeItems({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  return (
    <div className="flex flex-col gap-1.5 p-1.5">
      {items.map(([item, chineseItem]) => (
        <div
          key={chinese ? chineseItem : item}
          className="rounded-lg bg-muted px-3 py-2.5 text-sm"
        >
          {chinese ? chineseItem : item}
        </div>
      ))}
    </div>
  );
}

function ScrollFadeEdgeTags({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  return (
    <div className="flex w-max gap-1.5 p-1.5">
      {tags.map(([tag, chineseTag]) => (
        <div
          key={chinese ? chineseTag : tag}
          className="shrink-0 rounded-lg bg-muted px-3 py-2.5 text-sm"
        >
          {chinese ? chineseTag : tag}
        </div>
      ))}
    </div>
  );
}
```

```tsx
<div className="scroll-fade-b overflow-y-auto">{/* ... */}</div>
```

The edge utilities are scroll-aware. Start edges fade in after you scroll away from the start, and end edges fade out when you reach the end. Use `scroll-fade-t`, `scroll-fade-b`, `scroll-fade-l`, and `scroll-fade-r` for physical edges. Use `scroll-fade-s` and `scroll-fade-e` for logical inline edges that mirror in RTL.

## Fade size

The fade depth defaults to `12%` of the container, capped at `40px` so tall scrollers stay subtle. Use `scroll-fade-<number>` to set a fixed size on the spacing scale instead, the same way `scroll-mt-<number>` works.

### Example: scroll-fade-size

```tsx
// biome-ignore-all lint/a11y/noNoninteractiveTabindex: These named scroll viewports need keyboard access.
import type { ExampleProps } from "../types";
export default function ScrollFadeSize({ locale }: ExampleProps = {}) {
  const chinese = locale === "zh-CN";
  return (
    <div className="mx-auto flex w-full max-w-xs flex-col gap-6">
      <div className="flex flex-col gap-3">
        <div className="overflow-hidden rounded-[14px] ring-1 ring-border">
          <section
            tabIndex={0}
            aria-label={chinese ? "可滚动条目" : "Scrollable items"}
            className="scroll-fade no-scrollbar scroll-fade-4 h-48 overflow-y-auto"
          >
            <ScrollFadeSizeItems locale={locale} />
          </section>
        </div>
        <p className="text-center font-mono text-[0.9em] text-muted-foreground">
          scroll-fade-4
        </p>
      </div>
      <div className="flex flex-col gap-3">
        <div className="overflow-hidden rounded-[14px] ring-1 ring-border">
          <section
            tabIndex={0}
            aria-label={chinese ? "可滚动条目" : "Scrollable items"}
            className="scroll-fade no-scrollbar scroll-fade-24 h-48 overflow-y-auto"
          >
            <ScrollFadeSizeItems locale={locale} />
          </section>
        </div>
        <p className="text-center font-mono text-[0.9em] text-muted-foreground">
          scroll-fade-24
        </p>
      </div>
    </div>
  );
}

function ScrollFadeSizeItems({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  return (
    <div className="flex flex-col gap-1.5 p-1.5">
      {Array.from({ length: 8 }, (_, index) => index + 1).map((itemNumber) => (
        <div
          key={itemNumber}
          className="rounded-lg bg-muted px-3 py-2.5 text-sm"
        >
          {chinese ? "条目" : "Item"} {itemNumber}
        </div>
      ))}
    </div>
  );
}
```

```tsx
<div className="scroll-fade overflow-y-auto scroll-fade-24">{/* ... */}</div>
```

For one-off values, use an arbitrary length or percentage:

```tsx
<div className="scroll-fade overflow-y-auto scroll-fade-[15%]">{/* ... */}</div>
```

To fade opposite edges by different amounts, use the per-edge modifiers `scroll-fade-t-<number>`, `scroll-fade-b-<number>`, `scroll-fade-s-<number>`, and `scroll-fade-e-<number>`. They override `scroll-fade-<number>` on the edge they target and accept arbitrary values too.

```tsx
<div className="scroll-fade overflow-y-auto scroll-fade-b-8 scroll-fade-t-2">
  {/* ... */}
</div>
```

Use the logical `s`/`e` modifiers for horizontal scrollers so the sizes mirror in RTL.

The fade eases in and out over a fixed scroll distance rather than appearing instantly. That distance is the `--scroll-fade-reveal` variable, `96px` by default and independent of the fade depth. Lower it for a snappier reveal or raise it for a more gradual one:

```tsx
<div className="scroll-fade overflow-y-auto [--scroll-fade-reveal:64px]">
  {/* ... */}
</div>
```

## Disabling the fade

Use `scroll-fade-none` to remove the fade. It works in any class order, so the typical use is responsive or stateful:

```tsx
<div className="scroll-fade overflow-y-auto md:scroll-fade-none">
  {/* ... */}
</div>
```

### Example: scroll-fade-none

```tsx
// biome-ignore-all lint/a11y/noNoninteractiveTabindex: These named scroll viewports need keyboard access.
import type { ExampleProps } from "../types";
export default function ScrollFadeNone({ locale }: ExampleProps = {}) {
  const chinese = locale === "zh-CN";
  return (
    <div className="mx-auto flex min-w-0 max-w-xs flex-col gap-6">
      <div className="flex flex-col gap-3">
        <div className="overflow-hidden rounded-[14px] ring-1 ring-border">
          <section
            tabIndex={0}
            aria-label={chinese ? "可滚动条目" : "Scrollable items"}
            className="scroll-fade no-scrollbar h-48 overflow-y-auto"
          >
            <ScrollFadeNoneItems locale={locale} />
          </section>
        </div>
        <p className="text-center font-mono text-[0.9em] text-muted-foreground">
          scroll-fade
        </p>
      </div>
      <div className="flex flex-col gap-3">
        <div className="overflow-hidden rounded-[14px] ring-1 ring-border">
          <section
            tabIndex={0}
            aria-label={chinese ? "可滚动条目" : "Scrollable items"}
            className="scroll-fade no-scrollbar scroll-fade-none h-48 overflow-y-auto"
          >
            <ScrollFadeNoneItems locale={locale} />
          </section>
        </div>
        <p className="text-center font-mono text-[0.9em] text-muted-foreground">
          scroll-fade scroll-fade-none
        </p>
      </div>
    </div>
  );
}

function ScrollFadeNoneItems({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  return (
    <div className="flex flex-col gap-1.5 p-1.5">
      {Array.from({ length: 8 }, (_, index) => index + 1).map((itemNumber) => (
        <div
          key={itemNumber}
          className="rounded-lg bg-muted px-3 py-2.5 text-sm"
        >
          {chinese ? "条目" : "Item"} {itemNumber}
        </div>
      ))}
    </div>
  );
}
```

## Fallback

The scroll-aware behavior is implemented with [CSS scroll-driven animations](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll-driven_animations), with no JavaScript and no scroll listeners. In browsers that do not support scroll-driven animations, `scroll-fade` falls back to a static fade on both edges, and edge utilities fall back to a static fade on the selected edge.

Since the mask is applied to the scroll container itself, a visible scrollbar fades with the content at the edges. Pair `scroll-fade` with `no-scrollbar`, which ships in the same package, if you want to hide the scrollbar entirely.

## RTL

Set `dir="rtl"` on the container or use SUI’s [DirectionProvider](/docs/components/direction).

`scroll-fade-x` follows the reading direction. At rest, the start edge is crisp and the end edge fades. In RTL layouts that means a crisp right edge and a fade on the left, mirrored from LTR.

### Example: scroll-fade-rtl

```tsx
// biome-ignore-all lint/a11y/noNoninteractiveTabindex: These named scroll viewports need keyboard access.
import type { ExampleProps } from "../types";

const tags = [
  "تصميم",
  "هندسة",
  "تسويق",
  "منتج",
  "أبحاث",
  "مبيعات",
  "دعم",
  "عمليات",
  "مالية",
  "قانوني",
];
export default function ScrollFadeRtl({ locale }: ExampleProps = {}) {
  const chinese = locale === "zh-CN";
  return (
    <div
      className="mx-auto w-full max-w-xs overflow-hidden rounded-[14px] ring-1 ring-border"
      dir="rtl"
    >
      <section
        tabIndex={0}
        aria-label={chinese ? "从右到左滚动" : "Right-to-left scrolling"}
        className="scroll-fade-x no-scrollbar overflow-x-auto"
      >
        <div className="flex w-max gap-1.5 p-1.5">
          {tags.map((tag) => (
            <div
              key={tag}
              className="shrink-0 rounded-lg bg-muted px-3 py-2.5 text-sm"
            >
              {tag}
            </div>
          ))}
        </div>
      </section>
    </div>
  );
}
```

## Accessibility

Keep important controls outside the masked edges. Give a plain scrolling viewport `tabIndex={0}` and an accessible name when it needs keyboard access. The utility changes only the visual mask: it does not remove content from the accessibility tree or trap focus. Set the viewport’s background and frame on an outer wrapper.
