Rotate Carousel

A horizontal carousel where items rotate in 3D as you scroll

Last updated on

Edit on GitHub

Manual

Install the following dependencies:

npm install react-native-reanimated expo-haptics react-native-worklets @sbaiahmed1/react-native-blur

Copy 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