# Scroll API

> Coordinate nested scrolling, virtual keyboards, focus, progress, and imperative spring scrolling.

Web: https://velvet-ui.watermelons.workers.dev/docs/scroll-api

Scroll coordinates long content, virtual keyboards, programmatic travel, and the gesture handoff between a nested scroller and its parent sheet.

## Primitive tree

```tsx
<Scroll.Root componentRef={scrollApi}>
  <Scroll.View
    axis="y"
    safeArea="visual-viewport"
    onScrollProgress={({ progress, distance }) => {}}
  >
    <Scroll.Content><Article /></Scroll.Content>
  </Scroll.View>
</Scroll.Root>
```

## Scroll.Root and imperative API

`componentRef` receives `getProgress`, `getDistance`, `getAvailableDistance`, `scrollTo`, and `scrollBy`.

```tsx
scrollApi.current?.scrollTo({
  progress: 1,
  animationSettings: "smooth",
});

scrollApi.current?.scrollBy({
  distance: -160,
  animationSettings: { stiffness: 420, damping: 40 },
});
```

## Scroll.View props

| Prop | Type | Default |
| --- | --- | --- |
| `axis` | `x \| y` | `y` |
| `safeArea` | none, layout-viewport, visual-viewport | visual-viewport |
| `scrollGestureTrap` | boolean or string | false |
| `scrollGestureOvershoot` | boolean | true |
| `onScrollProgress` | progress callback | — |
| `onFocusInside` | policy object or callback | — |
| `nativeFocusScrollPrevention` | boolean | true |

## Scroll.Trigger

```tsx
<Scroll.Trigger action={{
  type: "scroll-to",
  progress: 1,
  animationSettings: "snappy",
}}>
  Jump to end
</Scroll.Trigger>
```

The action type is `scroll-to` or `scroll-by`. Supply either `distance` in pixels or normalized `progress`.

## Sheet handoff

```tsx
<Sheet.View side="bottom" tracks="auto">
  <Sheet.Content asChild>
    <Scroll.Root asChild>
      <Scroll.View><Scroll.Content><Feed /></Scroll.Content></Scroll.View>
    </Scroll.Root>
  </Sheet.Content>
</Sheet.View>
```

With `tracks="auto"`, the sheet reads the nearest scroll boundary. A downward drag dismisses only after the scroller reaches its start.

## Keyboard-safe forms

Use `safeArea="visual-viewport"` and include `interactive-widget=resizes-content` in the viewport meta tag. Keep the focused field and fixed composer inside the same Scroll.View.

```html
<meta
  name="viewport"
  content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content"
/>
```
