Skip to main content
Version: v4 (Reanimated v2)

Custom Handle

To override the default handle, you will need to pass the prop handleComponent to the BottomSheet component.

When you provide your own handle component, it will receive these animated props animatedIndex & animatedPosition that indicates the position and the index of the sheet.

You can extend your custom handle props interface with the provided BottomSheetHandleProps interface to expose animatedIndex & animatedPosition into your own interface.

Example

Here is an example of a custom handle component, but first you will need to install Redash:

Redash: The React Native Reanimated and Gesture Handler Toolbelt.

yarn add react-native-redash
import React, { useMemo } from "react";
import { StyleProp, StyleSheet, ViewStyle } from "react-native";
import { BottomSheetHandleProps } from "@gorhom/bottom-sheet";
import Animated, {
Extrapolate,
interpolate,
useAnimatedStyle,
useDerivedValue,
} from "react-native-reanimated";
import { toRad } from "react-native-redash";

// @ts-ignore
export const transformOrigin = ({ x, y }, ...transformations) => {
"worklet";
return [
{ translateX: x },
{ translateY: y },
...transformations,
{ translateX: x * -1 },
{ translateY: y * -1 },
];
};

interface HandleProps extends BottomSheetHandleProps {
style?: StyleProp<ViewStyle>;
}

const Handle: React.FC<HandleProps> = ({ style, animatedIndex }) => {
//#region animations
const indicatorTransformOriginY = useDerivedValue(() =>
interpolate(animatedIndex.value, [0, 1, 2], [-1, 0, 1], Extrapolate.CLAMP)
);
//#endregion

//#region styles
const containerStyle = useMemo(() => [styles.header, style], [style]);
const containerAnimatedStyle = useAnimatedStyle(() => {
const borderTopRadius = interpolate(
animatedIndex.value,
[1, 2],
[20, 0],
Extrapolate.CLAMP
);
return {
borderTopLeftRadius: borderTopRadius,
borderTopRightRadius: borderTopRadius,
};
});
const leftIndicatorStyle = useMemo(
() => ({
...styles.indicator,
...styles.leftIndicator,
}),
[]
);
const leftIndicatorAnimatedStyle = useAnimatedStyle(() => {
const leftIndicatorRotate = interpolate(
animatedIndex.value,
[0, 1, 2],
[toRad(-30), 0, toRad(30)],
Extrapolate.CLAMP
);
return {
transform: transformOrigin(
{ x: 0, y: indicatorTransformOriginY.value },
{
rotate: `${leftIndicatorRotate}rad`,
},
{
translateX: -5,
}
),
};
});
const rightIndicatorStyle = useMemo(
() => ({
...styles.indicator,
...styles.rightIndicator,
}),
[]
);
const rightIndicatorAnimatedStyle = useAnimatedStyle(() => {
const rightIndicatorRotate = interpolate(
animatedIndex.value,
[0, 1, 2],
[toRad(30), 0, toRad(-30)],
Extrapolate.CLAMP
);
return {
transform: transformOrigin(
{ x: 0, y: indicatorTransformOriginY.value },
{
rotate: `${rightIndicatorRotate}rad`,
},
{
translateX: 5,
}
),
};
});
//#endregion

// render
return (
<Animated.View
style={[containerStyle, containerAnimatedStyle]}
renderToHardwareTextureAndroid={true}
>
<Animated.View style={[leftIndicatorStyle, leftIndicatorAnimatedStyle]} />
<Animated.View
style={[rightIndicatorStyle, rightIndicatorAnimatedStyle]}
/>
</Animated.View>
);
};

export default Handle;

const styles = StyleSheet.create({
header: {
alignContent: "center",
alignItems: "center",
justifyContent: "center",
backgroundColor: "white",
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: "#fff",
},
indicator: {
position: "absolute",
width: 10,
height: 4,
backgroundColor: "#999",
},
leftIndicator: {
borderTopStartRadius: 2,
borderBottomStartRadius: 2,
},
rightIndicator: {
borderTopEndRadius: 2,
borderBottomEndRadius: 2,
},
});