Scroll Fades That Know Where They Are

Jun 26, 2026 Published

For lists, navigation, and other areas where content keeps growing, it is common to give the area a fixed height and make the overflowing content scroll.

On the web, CSS overflow covers much of this behavior. But overflow decides how overflowing content is handled. It does not decide how the edge of that area should look.

If the content simply cuts off at the edge, overflow is enough. But if you want to add a fade effect at the edge to gently show that more content continues, the problem changes a little. A static fade is always visible, so the top can still fade while you are already at the top, and the bottom can still fade while you are already at the bottom.

This is where Scroll Area components from Base UI and Radix UI fit nicely.

First, here is the comparison. The demo below shows three implementations of a similar side navigation. The Menu area is scrollable. When the examples are side by side, scrolling one of them scrolls the other two to the same relative position, which makes the edge behavior easier to compare. On narrow screens, the examples stack vertically and scroll independently.

No fade

/plain-overflow

Menu

Updates

Start

Install

Config

Nav

Scroll

A11y

Keys

Theme

Release

Migrate

Archive

More

UI

Demos

Kits

Changes

Roadmap

Help

Account
plain.tsxtsx
1<div className="flex h-64 flex-col border border-highlight-med bg-surface">2  <SidebarHeader />3  <div className="min-h-0 flex-1 overflow-y-auto px-3 py-3">4    <SidebarLinks />5  </div>6  <SidebarFooter />7</div>

CSS mask

/always-on-mask

Menu

Updates

Start

Install

Config

Nav

Scroll

A11y

Keys

Theme

Release

Migrate

Archive

More

UI

Demos

Kits

Changes

Roadmap

Help

Account
react-mask.tsxtsx
1<div className="flex h-64 flex-col border border-highlight-med bg-surface">2  <SidebarHeader />3  <div className="min-h-0 flex-1 overflow-y-auto px-3 py-3 mask-linear-[to_bottom,transparent_0,black_2.75rem,black_calc(100%-2.75rem),transparent_100%] mask-no-repeat">4    <SidebarLinks />5  </div>6  <SidebarFooter />7</div>

Base UI

/state-aware-scroll-area
Account
base-ui.tsxtsx
1<div className="flex h-64 flex-col border border-highlight-med bg-surface">2  <SidebarHeader />3  <ScrollArea.Root className="relative min-h-0 flex-1 overflow-hidden">4    <ScrollArea.Viewport className="h-full outline-none mask-linear-[to_bottom,transparent_0,black_min(2.75rem,var(--scroll-area-overflow-y-start)),black_calc(100%-min(2.75rem,var(--scroll-area-overflow-y-end,2.75rem))),transparent_100%] mask-no-repeat">5      <ScrollArea.Content className="px-3 py-3">6        <SidebarLinks />7      </ScrollArea.Content>8    </ScrollArea.Viewport>9    <ScrollArea.Scrollbar className="flex w-2 justify-center py-2 opacity-0 transition-opacity data-hovering:opacity-100 data-scrolling:opacity-100">10      <ScrollArea.Thumb className="w-1 rounded-full bg-highlight-high" />11    </ScrollArea.Scrollbar>12  </ScrollArea.Root>13  <SidebarFooter />14</div>

No fade is the simplest implementation. The middle area uses overflow-y-auto, and it works. The edge behavior is left to the scrollbar and to the way the content cuts off.

CSS mask looks much better. For many small UIs, CSS alone is enough. But the same fade is always applied, so it cannot express whether you are at the top or at the bottom. Even when there is no content above, the top edge still fades.

With Base UI Scroll Area, you can receive scroll state as CSS custom properties. By using --scroll-area-overflow-y-start and --scroll-area-overflow-y-end in the mask, you can remove the top fade at the top edge and remove the bottom fade at the bottom edge.

These two values represent the distance from the viewport's top edge and bottom edge, in pixels. If maxScroll is scrollHeight - clientHeight, the relationship is roughly this:

Variable

Top

Middle

Bottom

--scroll-area-overflow-y-start

0px

scrollTop

maxScroll

--scroll-area-overflow-y-endmaxScrollmaxScroll - scrollTop0px

For example, when you are at the top edge, --scroll-area-overflow-y-start is 0px, so the top fade can also be 0px. As you scroll down, that value increases. When you reach the bottom edge, --scroll-area-overflow-y-end becomes 0px.

In this demo, the fade has a maximum width and uses an expression like min(2.75rem, var(--scroll-area-overflow-y-start)), so the fade width changes with the scroll position.

Scroll Area components are a useful option when you want to pass scroll area state back into CSS.