# Scroll area

Host: Base UI. Path: transitions. Preset: `fade`. Built against [@base-ui/react 1.5.0](https://www.npmjs.com/package/@base-ui/react/v/1.5.0).

The one host here that publishes no open and closed pair. Base UI keeps this scrollbar mounted and only reports whether you are hovering or scrolling, so hiding it is the page's job and the state attribute has to come from the app. That is what data-motion-state is for.

The page this file mirrors mounts the component twice, once with the attribute and once without, so the only difference between the two is the library.

Upstream documentation: [Base UI documentation](https://base-ui.com/react/components/scroll-area)

The same component in other hosts: [Radix UI](/radix/scroll-area.md).

## Where the attribute goes

The parts this demo composes, and the value each one carries. Everything inside an animated part is that part's content, so the tree stops there.

```text
ScrollArea.Root
  ScrollArea.Viewport
    ScrollArea.Content
  ScrollArea.Scrollbar  data-motion="fade" data-motion-state
```

## How the match works

| Step | Value |
| --- | --- |
| host writes | `nothing. only data-hovering and data-scrolling` |
| easeful matches | `[data-motion~="fade"][data-motion-state="closed"]` |
| what runs | `transition: opacity 150ms` |
| you write | `<ScrollArea.Scrollbar data-motion="fade" data-motion-state={...} />` |

## Every value that works here

Every value the manifest allows on this host, single presets and the pairs that compose. The first is what this component ships with. Both stages are the same component, so anything that changes between them is the attribute.

| Value | Link |
| --- | --- |
| `fade` | the default |
| `scale-fade` | [scale-fade](/base-ui/scroll-area#motion=scale-fade) |
| `slide-up` | [slide-up](/base-ui/scroll-area#motion=slide-up) |
| `fade slide-up` | [fade slide-up](/base-ui/scroll-area#motion=fade%20slide-up) |
| `scale-fade slide-up` | [scale-fade slide-up](/base-ui/scroll-area#motion=scale-fade%20slide-up) |

## Source

`apps/docs/components/demos/base-ui/scroll-area.tsx`

```tsx
"use client"

import { useEffect, useRef, useState } from "react"
import { ScrollArea } from "@base-ui/react/scroll-area"
import type { DemoProps } from "@/components/demos/props"

/* The one host in this matrix that publishes no open and closed state.
 *
 * Base UI's Scrollbar stays mounted permanently and carries only interaction
 * flags: data-orientation, data-has-overflow-y, data-hovering, data-scrolling.
 * There is no visible/hidden pair the way Radix's scrollbar has one, so easeful
 * has nothing to match on and the bar simply never leaves. Hiding it is the
 * application's job here, not the library's.
 *
 * So this demo drives data-motion-state, the escape hatch easeful documents for
 * exactly this case, from a scroll handler with the same 900ms delay the Radix
 * demo passes as scrollHideDelay. Both columns get the state, so the only
 * difference between them is still the attribute and therefore the animation:
 * without it the bar cuts, with it the bar fades. */
const HIDE_DELAY = 900

export function BaseUiScrollArea({ motion }: DemoProps) {
  const [shown, setShown] = useState(false)
  const timer = useRef<number | undefined>(undefined)

  useEffect(() => () => window.clearTimeout(timer.current), [])

  function onScroll() {
    setShown(true)
    window.clearTimeout(timer.current)
    timer.current = window.setTimeout(() => setShown(false), HIDE_DELAY)
  }

  return (
    <ScrollArea.Root className="scroller">
      <ScrollArea.Viewport className="scroller__viewport" onScroll={onScroll}>
        <ScrollArea.Content>
          <p>
            Scroll this box, then stop. Watch the bar on the right as it leaves rather than as it
            arrives.
          </p>
          <p>
            Base UI never hides this scrollbar itself. It reports whether you are hovering or
            scrolling and leaves the rest to the page, so the state attribute here comes from the
            demo rather than from the host.
          </p>
          <p>Without the attribute the bar cuts out on the frame the delay elapses.</p>
          <p>Keep going so there is room to scroll.</p>
          <p>One last line.</p>
        </ScrollArea.Content>
      </ScrollArea.Viewport>
      <ScrollArea.Scrollbar
        className="scroller__bar"
        orientation="vertical"
        data-motion={motion}
        data-motion-state={shown ? "open" : "closed"}
      >
        <ScrollArea.Thumb className="scroller__thumb" />
      </ScrollArea.Scrollbar>
    </ScrollArea.Root>
  )
}
```
