スクロールエリアの端にも状態がある

2026年6月26日 公開

リストやナビゲーションのように、内容が増えていく領域は、あらかじめ高さを決めてスクロールさせることがあります。

ウェブでは、こうした挙動の多くをCSSのoverflowプロパティで表現できます。ただし、overflowが決めてくれるのは、はみ出した内容の扱い方です。その領域の端をどう見せるかまでは決めてくれません。

内容が端で切れるだけなら、overflowを指定するだけで十分です。けれど、端にfade effectをかけて「まだ続きがある」ことを丁寧に見せようとすると、話が少し変わります。常に同じfadeを表示するだけでは、上端にいるときにも上側が薄くなり、下端まで来ているときにも下側が薄くなってしまいます。

こういう時に、Base UIやRadix UIが提供しているScroll Areaコンポーネントを使うと自然に実装できます。

まずは、実際に見てみましょう。下のデモでは、同じようなサイドナビゲーションを3つの実装で並べています。Menuがスクロールできるようになっています。横に並んでいる時は、どれかひとつをスクロールすると他の2つも同じ位置までスクロールするので、端の見え方の違いを比べやすいと思います。スマホなど幅の狭い環境の場合は縦積みになり、その場合は別々にスクロールします。

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 は、一番素朴な実装です。中央の領域に overflow-y-auto を指定すれば動きます。ただ、端の情報はスクロールバーや内容の切れ方に任せることになります。

CSS mask は、見た目としてはかなり良くなります。CSSだけで済むので、小さなUIならこれで十分なことも多いです。ただし、常に同じfadeがかかるため、いま上端にいるのか、下端にいるのかまでは表現できません。まだ上に内容がないときにも、上側が薄くなってしまいます。

Base UI のScroll Areaを使うと、スクロール位置に応じた状態をCSS custom propertyとして受け取れます。--scroll-area-overflow-y-start--scroll-area-overflow-y-end をmaskに使うことで、上端にいるときは上のfadeを消し、下端にいるときは下のfadeを消せます。

この2つの値は、それぞれ「viewportの上端からどれくらい離れているか」と「viewportの下端からどれくらい離れているか」をpxで表します。maxScrollscrollHeight - clientHeight とすると、だいたい次のような関係になります。

変数

上端

途中

下端

--scroll-area-overflow-y-start

0px

scrollTop

maxScroll

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

たとえば上端にいるときは --scroll-area-overflow-y-start0px になるので、上側のfade幅も 0px にできます。少し下にスクロールするとこの値が増え、下端まで来ると --scroll-area-overflow-y-end0px になります。

今回のデモでは、min(2.75rem, var(--scroll-area-overflow-y-start)) のように最大幅を決めつつ、スクロール位置に応じてfade幅が変わるようにしています。

Scroll Area componentを、スクロール領域の状態をCSSに渡したいときに便利な選択肢でした。