How do you programmatically scroll a React Native ScrollView to a given section, such as one clause on a terms-of-service page?
answer
- a ref to the ScrollView
- scrollTo takes an options object
- animated defaults to true
- offsets come from onLayout
- scrollEnabled only blocks touch
basics
~20 sKeep a ref to the React Native ScrollView and call scrollTo({x, y, animated}) or scrollToEnd({animated}) on it. Take a section's y offset from its onLayout event when it is a direct child; animated defaults to true.
solid answer
~40 sAttach a ref (typed `useRef<ScrollViewInstance>(null)` with the 0.87 TypeScript API) and call `ref.current?.scrollTo({y, animated})`. Omitted `x` or `y` count as `0`, and `animated` defaults to `true`; the old positional form `scrollTo(y, x, animated)` is deprecated and warns. `scrollToEnd({animated})` goes to the bottom of a vertical view or the right end of a horizontal one. For a terms page with a table of contents, record each clause's `y` from its `onLayout` — for a direct child that is its offset inside the content container, padding included — and scroll to it on press. `scrollEnabled={false}` only stops touch scrolling; `scrollTo` still works. The `contentOffset` prop sets the starting offset declaratively.
code
tsx · 56 linesimport {useRef} from 'react';
import {
Pressable,
ScrollView,
StyleSheet,
Text,
View,
type ScrollViewInstance,
} from 'react-native';
type Clause = {id: string; title: string; body: string};
export function TermsWithContents({clauses}: {clauses: Clause[]}) {
const scrollRef = useRef<ScrollViewInstance>(null);
const offsets = useRef(new Map<string, number>());
const jumpTo = (id: string) => {
const y = offsets.current.get(id);
if (y != null) {
scrollRef.current?.scrollTo({y, animated: true});
}
};
return (
<View style={styles.screen}>
<View style={styles.contents}>
{clauses.map(clause => (
<Pressable key={clause.id} onPress={() => jumpTo(clause.id)}>
<Text style={styles.link}>{clause.title}</Text>
</Pressable>
))}
</View>
<ScrollView ref={scrollRef} contentContainerStyle={styles.content}>
{clauses.map(clause => (
// Direct child: layout.y is its offset inside the content container.
<View
key={clause.id}
onLayout={e =>
offsets.current.set(clause.id, e.nativeEvent.layout.y)
}>
<Text style={styles.title}>{clause.title}</Text>
<Text>{clause.body}</Text>
</View>
))}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
screen: {flex: 1},
contents: {flexDirection: 'row', flexWrap: 'wrap', gap: 8, padding: 8},
link: {textDecorationLine: 'underline'},
content: {padding: 16, gap: 24},
title: {fontWeight: '600', marginBottom: 4},
});go deeper
Recall the pieces: a ref on the ScrollView, then scrollTo with an options object or scrollToEnd. animated is on unless you turn it off.
Explain where offsets come from: onLayout of direct children, measured inside the content container. Know that scrollEnabled only blocks touch and that the positional scrollTo form is deprecated.
Handle timing and layout change: store offsets in a ref that onLayout keeps current, and trigger scrollToEnd from onContentSizeChange rather than straight after a state update.
Decide when programmatic scrolling is the right tool versus splitting a long document into separate screens, weighing linkable sections and accessibility against one continuous page.
## The imperative API A `ScrollView` exposes imperative methods through its ref: - **`scrollTo(options)`** — scrolls to an `{x, y}` offset, animated or not; - **`scrollToEnd(options)`** — scrolls to the bottom of a vertical scroll view, or to the right end of a horizontal one; - **`flashScrollIndicators()`** — shows the scroll indicators briefly, a hint that the content scrolls. In React Native 0.87 the Strict TypeScript API is the default, and the root `react-native` package exports a `ScrollViewInstance` type for the ref: `useRef<ScrollViewInstance>(null)`. ## scrollTo in detail The signature takes an options object `{x?, y?, animated?}`: 1. **Missing coordinates are `0`.** `scrollTo({y: 600})` scrolls to `x: 0, y: 600`. 2. **`animated` defaults to `true`.** Only an explicit `animated: false` jumps instantly. 3. **The positional form is deprecated.** For historical reasons `scrollTo(y, x, animated)` still works, but it logs a warning and the docs say it should not be used, because the order (`y` before `x`) is easy to get wrong. 4. **Nothing happens before mount.** If the native scroll view does not exist yet, the call returns without scrolling. `scrollToEnd` takes `{animated?}`, also defaulting to `true`. ## Where the offset comes from For a terms-of-service page with a table of contents, each clause needs a target `y`: - render each clause as a **direct child** of the `ScrollView`; - give it an `onLayout` handler and store `event.nativeEvent.layout.y` by clause id; - for a direct child, that `y` is measured inside the **content container**, so it already includes the container's top padding and is exactly the scroll offset that brings the clause to the top. If a clause is nested inside another view, its `onLayout` `y` is relative to that parent, not to the content container, and you must add the parent's offset. Keeping clauses as direct children avoids this. Offsets change when the layout changes — font scaling, rotation, content loaded later — and `onLayout` fires again in those cases, so storing the latest value in a ref keeps the table of contents correct. ## Scrolling after content loads A common need is "scroll to the end once the content has rendered". Calling `scrollToEnd` right after setting state can run before the new content is laid out. The `onContentSizeChange` prop is called with the content width and height whenever the content container's size changes, which makes it a reliable trigger for `scrollToEnd`. ## Related props | Prop or method | What it does | |---|---| | `contentOffset` | starting offset, set declaratively | | `scrollEnabled={false}` | blocks touch scrolling only; `scrollTo` still works | | `onContentSizeChange` | reports new content width and height, useful before `scrollToEnd` | | `scrollToEnd({animated})` | bottom (vertical) or right end (horizontal) | | `flashScrollIndicators()` | briefly shows the scroll indicators | `scrollEnabled={false}` combined with `scrollTo` gives a step-by-step flow — say, an onboarding pager driven only by Next buttons. ## Horizontal scroll views Everything above applies on the other axis. For a horizontal `ScrollView` — say, a row of plan cards on a settings screen — store `layout.x` from each card's `onLayout` and call `scrollTo({x, animated: true})`; `y` is then simply left out and treated as `0`. `scrollToEnd` scrolls to the right end instead of the bottom. Snapping and paging props are a separate concern from this imperative API. ## Common mistakes - Using `scrollTo(0, 600)` expecting `x: 0, y: 600`; the deprecated form reads it as `y: 0, x: 600`. - Reading `pageY` from a press event and passing it to `scrollTo`; that is a window coordinate, not a content offset. - Storing offsets from children nested in wrappers and scrolling to the wrong place. - Calling `scrollToEnd` before the content has laid out.
- Why is scrollTo(0, 600) a bug?The numeric form is the deprecated positional signature `scrollTo(y, x, animated)`, so it reads the first number as `y` and the second as `x`: it scrolls to `y: 0, x: 600`, not the other way round. React Native also logs a deprecation warning. Use the options object: `scrollTo({x: 0, y: 600})`.
- How do you scroll to the end only after new content has rendered?Call `scrollToEnd` from `onContentSizeChange`, which the `ScrollView` calls with the content width and height whenever its content container's size changes. That guarantees the new content has been laid out, whereas calling it straight after a state update can run against the old content size.
saying these in an interview costs you the question
- scrollTo jumps instantly unless you pass animated: true.
- scrollEnabled={false} also blocks scrollTo and scrollToEnd.
- scrollTo(0, 600) scrolls to x 0 and y 600.
- The pageY of a press event is the right offset for scrollTo.
- A nested child's onLayout y is always relative to the ScrollView content.