Rotate Carousel
A horizontal carousel where items rotate in 3D as you scroll
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated expo-haptics react-native-worklets @sbaiahmed1/react-native-blurCopy and paste the following code into your project.
component/molecules/rotate-carousel
import { Dimensions, StyleSheet, View } from "react-native";import React from "react";import type { RotateCarouselItemProps, RotateCarouselProps } from "./types";import { BlurView } from "@sbaiahmed1/react-native-blur";import Animated, { interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, Extrapolation,} from "react-native-reanimated";const { width: SCREEN_WIDTH } = Dimensions.get("window");const ITEM_WIDTH = SCREEN_WIDTH * 0.7;const SPACING = 20;const SIDE_SPACING = (SCREEN_WIDTH - ITEM_WIDTH) / 2;const RotateCarouselItem = <ItemT,>({ item, index, scrollX, renderItem, itemWidth = ITEM_WIDTH, spacing = SPACING, rotatePercentage = 90,}: RotateCarouselItemProps<ItemT>) => { const animatedStyle = useAnimatedStyle(() => { const inputRange = [ (index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth, ]; const scale = interpolate( scrollX.value, inputRange, [0.8, 1, 0.8], Extrapolation.CLAMP, ); const rotateY = interpolate( scrollX.value, inputRange, [-rotatePercentage, 0, -rotatePercentage], Extrapolation.CLAMP, ); const opacity = interpolate( scrollX.value, inputRange, [0.5, 1, 0.5], Extrapolation.CLAMP, ); const translateX = interpolate( scrollX.value, inputRange, [itemWidth * 0.2, 0, -itemWidth * 0.2], Extrapolation.CLAMP, ); return { transform: [ { perspective: 1200 }, { scale }, { rotateY: `${rotateY}deg` }, { translateX }, ], opacity, }; }); return ( <Animated.View style={[ styles.itemContainer, animatedStyle, { width: itemWidth, }, ]} > <View style={[ styles.itemContent, { width: itemWidth - spacing * 2, }, ]} > {renderItem({ item, index })} </View> </Animated.View> );};const RotateCarousel = <ItemT,>({ data, renderItem, horizontalSpacing = SIDE_SPACING, itemWidth = ITEM_WIDTH, spacing = SPACING, rotatePercentage = 90, keyExtractor,}: RotateCarouselProps<ItemT>) => { const scrollX = useSharedValue(0); const onScroll = useAnimatedScrollHandler({ onScroll: (event) => { scrollX.value = event.contentOffset.x; }, }); return ( <Animated.FlatList data={data} showsHorizontalScrollIndicator={false} onScroll={onScroll} scrollEventThrottle={16} keyExtractor={ keyExtractor ? (item, index) => keyExtractor(item, index) : (_, index) => index.toString() } horizontal pagingEnabled snapToInterval={itemWidth} decelerationRate="fast" contentContainerStyle={{ paddingHorizontal: horizontalSpacing, marginTop: 20, marginBottom: 30, }} style={{ flexGrow: 0, }} renderItem={({ item, index }) => ( <RotateCarouselItem item={item} index={index} scrollX={scrollX} renderItem={renderItem} itemWidth={itemWidth} spacing={spacing} rotatePercentage={rotatePercentage} /> )} /> );};const styles = StyleSheet.create({ itemContainer: { justifyContent: "center", alignItems: "center", }, itemContent: { borderRadius: 20, overflow: "hidden", shadowColor: "#000", shadowOffset: { width: 0, height: 8, }, shadowOpacity: 0.3, shadowRadius: 12, elevation: 10, }, blurOverlay: { position: "absolute", overflow: "hidden", },});export { RotateCarousel, RotateCarouselItemProps, RotateCarouselProps };Usage
import { View, Text, StyleSheet, Image, Dimensions } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";import { StatusBar } from "expo-status-bar";import { SymbolView } from "expo-symbols";import { RotateCarousel } from "@/components/molecules/rotate-carousel";import { useFonts } from "expo-font";const { width: SCREEN_WIDTH } = Dimensions.get("window");export default function App() { const [fontLoaded] = useFonts({ SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"), HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"), }); const POSTERS = [ "https://i.pinimg.com/1200x/5a/ad/c6/5aadc6ef06807b24de9d0ea236c28978.jpg", "https://i.pinimg.com/736x/ea/9f/97/ea9f9778de29809187e40b6b12f3ca28.jpg", "https://i.pinimg.com/736x/c7/ad/93/c7ad937da5f3796492a4ce378db61700.jpg", "https://i.pinimg.com/736x/d8/af/cc/d8afcc936c977bab53cec723a2f2fc1c.jpg", ]; return ( <GestureHandlerRootView style={styles.container}> <StatusBar style="light" /> <View style={styles.header}> <View> <Text style={[ styles.title, { fontFamily: fontLoaded ? "HelveticaNowDisplay" : undefined, }, ]} > Gallery </Text> <Text style={[ styles.subtitle, { fontFamily: fontLoaded ? "SfProRounded" : undefined, }, ]} > Swipe to explore </Text> </View> <View style={styles.iconButton}> <SymbolView name="square.grid.2x2" size={22} tintColor="#fff" /> </View> </View> <RotateCarousel data={POSTERS.map((uri) => ({ image: { uri } }))} renderItem={({ item }) => ( <Image source={item.image} style={styles.posterImage} /> )} rotatePercentage={150} /> <View style={styles.footer}> <SymbolView name="hand.draw" size={18} tintColor="rgba(255,255,255,0.5)" /> <Text style={[ styles.footerText, { fontFamily: fontLoaded ? "SfProRounded" : undefined, }, ]} > Swipe left or right </Text> </View> </GestureHandlerRootView> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#0a0a0a", }, header: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingHorizontal: 25, paddingTop: 60, paddingBottom: 20, }, title: { fontSize: 32, fontWeight: "700", color: "#fff", letterSpacing: -0.5, }, subtitle: { fontSize: 14, color: "rgba(255,255,255,0.5)", marginTop: 2, }, iconButton: { width: 44, height: 44, borderRadius: 22, backgroundColor: "rgba(255,255,255,0.1)", justifyContent: "center", alignItems: "center", }, posterImage: { width: 250, height: 390, borderRadius: 16, }, footer: { flexDirection: "row", justifyContent: "center", alignItems: "center", gap: 8, paddingBottom: 40, }, footerText: { fontSize: 13, color: "rgba(255,255,255,0.5)", },});Props
RotateCarouselItemProps
React Native Reanimated
Expo Haptics
React Native Worklets
React Native Blur
