Reference

Scroll API

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

View as Markdown

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

PropTypeDefault
axisx | yy
safeAreanone, layout-viewport, visual-viewportvisual-viewport
scrollGestureTrapboolean or stringfalse
scrollGestureOvershootbooleantrue
onScrollProgressprogress callback
onFocusInsidepolicy object or callback
nativeFocusScrollPreventionbooleantrue

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"
/>