0

I created a custom sheet in react native by following an online tutorial. Beyond the typical scroll view bottom sheet, I decided to add a header and footer component in the bottom sheet with a scroll view in between. The bottom sheet functions as it should, except for one issue. When dragging to close the bottom sheet, the footer remains fixed on the bottom of the sheet until the header collapses onto it.

The behaviour that I was expecting is for the footer component to move in tandem with the rest of the sheet when dragging to close. I tried wrapping my footer component in an animated view or animating the footer itself, but the behaviour did not change.

I have attached my code for the custom sheet for reproducibility:

import { forwardRef, useCallback, useImperativeHandle, useState } from "react";
import { Dimensions, View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
 runOnJS,
 useAnimatedScrollHandler,
 useAnimatedStyle,
 useSharedValue,
 withSpring,
 withTiming,
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import BackDrop from "../Backdrop/BackDrop";
import { styledContainer, styledContentContainer, styledIndicator, styledIndicatorContainer } from "./styles/BottomSheetStyles";
export const BottomSheet = forwardRef(
 (
 {
 snapTo,
 backgroundColor,
 backdropColour,
 type,
 children,
 headerComponent,
 footerComponent,
 ...props
 },
 ref
 ) => {
 const insets = useSafeAreaInsets();
 const { height } = Dimensions.get("screen");
 const percentage = parseFloat(snapTo) / 100;
 const openHeight = height - height * percentage;
 const closeHeight = height;
 const topAnimation = useSharedValue(closeHeight);
 const context = useSharedValue(0);
 const scrollBegin = useSharedValue(0);
 const scrollY = useSharedValue(0);
 const [enabledScroll, setEnableScroll] = useState(true);
 const expand = useCallback(() => {
 "worklet";
 topAnimation.value = withTiming(openHeight);
 }, [openHeight, topAnimation]);
 const close = useCallback(() => {
 "worklet";
 topAnimation.value = withTiming(closeHeight);
 }, [closeHeight, topAnimation]);
 useImperativeHandle(ref, () => ({ expand, close }), [expand, close]);
 const animationStyle = useAnimatedStyle(() => {
 const top = topAnimation.value;
 return { top };
 });
 const pan = Gesture.Pan()
 .onBegin(() => {
 context.value = topAnimation.value;
 })
 .onUpdate((event) => {
 if (event.translationY < 0) {
 topAnimation.value = withSpring(openHeight, {
 damping: 100,
 stiffness: 400,
 });
 } else {
 topAnimation.value = withSpring(event.translationY + context.value, {
 damping: 100,
 stiffness: 400,
 });
 }
 })
 .onEnd(() => {
 if (topAnimation.value > openHeight + 50) {
 topAnimation.value = withSpring(closeHeight, {
 damping: 100,
 stiffness: 400,
 });
 } else {
 topAnimation.value = withSpring(openHeight, {
 damping: 100,
 stiffness: 400,
 });
 }
 });
 const onScroll = useAnimatedScrollHandler({
 onBeginDrag: (event) => {
 scrollBegin.value = event.contentOffset.y;
 },
 onScroll: (event) => {
 scrollY.value = event.contentOffset.y;
 },
 });
 const panScroll = Gesture.Pan()
 .onBegin(() => {
 context.value = topAnimation.value;
 })
 .onUpdate((event) => {
 if (event.translationY < 0) {
 runOnJS(setEnableScroll)(true);
 topAnimation.value = withSpring(openHeight, {
 damping: 100,
 stiffness: 400,
 });
 } else if (event.translationY > 0 && scrollY.value === 0) {
 runOnJS(setEnableScroll)(false);
 topAnimation.value = withSpring(
 Math.max(
 context.value + event.translationY - scrollBegin.value, 
 openHeight
 ), 
 {
 damping: 100,
 stiffness: 400,
 }
 );
 }
 })
 .onEnd(() => {
 runOnJS(setEnableScroll)(true);
 if (topAnimation.value > openHeight + 50) {
 topAnimation.value = withSpring(closeHeight, {
 damping: 100,
 stiffness: 400,
 });
 } else {
 topAnimation.value = withSpring(openHeight, {
 damping: 100,
 stiffness: 400,
 });
 }
 });
 const scrollViewGesture = Gesture.Native();
 return (
 <>
 <BackDrop
 topAnimation={topAnimation}
 closeHeight={closeHeight}
 openHeight={openHeight}
 close={close}
 backdropColour={backdropColour}
 />
 <GestureDetector gesture={pan}>
 <Animated.View
 style={[
 styledContainer,
 animationStyle,
 {
 backgroundColor: backgroundColor,
 paddingBottom: insets.bottom,
 },
 ]}
 >
 <View style={styledIndicatorContainer}>
 <View style={styledIndicator} />
 </View>
 <View style={styledContentContainer}>
 <View>{headerComponent}</View>
 {type === "scroll" ? (
 <GestureDetector
 gesture={Gesture.Simultaneous(panScroll, scrollViewGesture)}
 >
 <Animated.ScrollView
 {...props}
 bounces={false}
 scrollEventThrottle={16}
 onScroll={onScroll}
 scrollEnabled={enabledScroll}
 >
 {children}
 </Animated.ScrollView>
 </GestureDetector>
 ) : (
 children
 )}
 <View>{footerComponent}</View>
 </View>
 </Animated.View>
 </GestureDetector>
 </>
 );
 }
);
Jatin Bhuva
1,9361 gold badge7 silver badges34 bronze badges
asked Jul 7, 2025 at 10:11

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.