Skip to main content

Position tracking

Use onPositionChange to observe the sheet’s current position. It is a standard native event; read the distance from the bottom of the sheet’s host to the top of the sheet from event.nativeEvent.position, in layout units (points on iOS, dp on Android, and CSS pixels on the web). For a modal sheet the host fills the screen; for an inline sheet it is the sheet’s container. 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>

A lazy sheet emits no onPositionChange events while it is unmounted, from the moment it settles at a closed detent until it opens again. Handle the closed state from onSettle if you need it.

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.