Skip to main content

Position tracking

Use onPositionChange to observe the sheet’s current position. It is a standard native event; read the distance in pixels from the bottom of the screen to the top of the sheet from event.nativeEvent.position. The same event also carries event.nativeEvent.index—the fractional detent index in 0..(detents.length - 1) (0 at the shortest detent, 1 at the next, and so on, interpolated in between)—the continuous counterpart of onIndexChange, handy for driving a backdrop or per-detent animation without knowing the sheet’s height.

<BottomSheet // Or `ModalBottomSheet`.
index={index}
onIndexChange={setIndex}
surface={/* ... */}
onPositionChange={(event) => {
console.log(event.nativeEvent.position, event.nativeEvent.index);
}}
>
{/* ... */}
</BottomSheet>

With Reanimated

To keep the latest position in a Reanimated shared value, update it from the callback:

const position = useSharedValue(0);
<BottomSheet
index={index}
onIndexChange={setIndex}
surface={/* ... */}
onPositionChange={(event) => {
position.value = event.nativeEvent.position;
}}
>
{/* ... */}
</BottomSheet>

Because onPositionChange is a native event, you can also handle it on the UI thread. Pass Animated.createAnimatedComponent to wrapNativeView—the library applies it to the native sheet view—and give onPositionChange a worklet handler from useEvent:

import type { NativeSyntheticEvent } from 'react-native';
import Animated, { useEvent, useSharedValue } from 'react-native-reanimated';
import {
BottomSheet,
type PositionChangeEventData,
} from '@swmansion/react-native-bottom-sheet';
const position = useSharedValue(0);
const detentIndex = useSharedValue(0);

const onPositionChange = useEvent<
NativeSyntheticEvent<PositionChangeEventData>
>(
(event) => {
'worklet';
position.value = event.position;
detentIndex.value = event.index;
},
['onPositionChange']
);
<BottomSheet // Or `ModalBottomSheet`.
index={index}
onIndexChange={setIndex}
surface={/* ... */}
wrapNativeView={Animated.createAnimatedComponent}
onPositionChange={onPositionChange}
>
{/* ... */}
</BottomSheet>

wrapNativeView keeps the animated wrapper on the native sheet view itself, so the worklet binds on first render—for both inline and modal sheets—without the library depending on Reanimated. Pass a stable function (such as Animated.createAnimatedComponent), not an inline lambda.

Inside the worklet, Reanimated unwraps the native event, so you read event.position directly rather than event.nativeEvent.position. The handler runs on the UI thread on every frame the sheet moves.