Cinematic Carousel
A horizontal carousel with a cinematic feel
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated @sbaiahmed1/react-native-blurCopy and paste the following code into your project.
component/molecules/cinematic-carousel
import { Dimensions, StyleSheet, View } from "react-native";import React from "react";import { CinematicCarouselItemProps, CinematicCarouselProps } from "./types";import { BlurView } from "@sbaiahmed1/react-native-blur";import Animated, { interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, Extrapolation, useAnimatedProps,} from "react-native-reanimated";const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);const { width: SCREEN_WIDTH } = Dimensions.get("window");const ITEM_WIDTH = SCREEN_WIDTH * 0.75;const SPACING = 20;const SIDE_SPACING = (SCREEN_WIDTH - ITEM_WIDTH) / 2;const CarouselItem = <ItemT,>({ item, index, scrollX, renderItem, itemWidth = ITEM_WIDTH, spacing = SPACING,}: CinematicCarouselItemProps<ItemT>) => { const animatedStyle = useAnimatedStyle(() => { const inputRange = [ (index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth, ]; const scale = interpolate( scrollX.value, inputRange, [0.85, 1, 0.85], Extrapolation.CLAMP, ); const opacity = interpolate( scrollX.value, inputRange, [0.5, 1, 0.5], Extrapolation.CLAMP, ); const rotateY = interpolate( scrollX.value, inputRange, [50, 0, -50], Extrapolation.CLAMP, ); const skewY = interpolate( scrollX.value, inputRange, [5, 0.5, -5], Extrapolation.CLAMP, ); const scaleY = interpolate( scrollX.value, inputRange, [0.9, 1, 0.9], Extrapolation.CLAMP, ); const translateX = interpolate( scrollX.value, inputRange, [spacing, 0, -spacing], Extrapolation.CLAMP, ); return { transform: [ { perspective: 400 }, { rotateY: `${rotateY}deg` }, { skewY: `${skewY}deg` }, { translateX }, { scaleY }, ], opacity, }; }); const animatedBlurProps = useAnimatedProps(() => { const inputRange = [ (index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth, ]; const blurIntensity = interpolate( scrollX.value, inputRange, [25, 0, 25], Extrapolation.CLAMP, ); return { blurAmount: blurIntensity, }; }); return ( <Animated.View style={[styles.itemContainer, animatedStyle, { width: itemWidth }]} > <View style={styles.contentWrapper}> {renderItem({ item, index })} <AnimatedBlurView style={[StyleSheet.absoluteFillObject, styles.blurOverlay]} blurType="regular" animatedProps={animatedBlurProps} // reducedTransparencyFallbackColor="" /> </View> </Animated.View> );};const CinematicCarousel = <ItemT,>({ data, renderItem, horizontalSpacing = SIDE_SPACING, itemWidth = ITEM_WIDTH, spacing = SPACING,}: CinematicCarouselProps<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={(_, index) => index.toString()} horizontal pagingEnabled snapToInterval={itemWidth} decelerationRate="fast" contentContainerStyle={{ paddingHorizontal: horizontalSpacing, marginTop: 40, marginBottom: 20, bottom: 2, }} style={{ flexGrow: 0, }} renderItem={({ item, index }) => ( <CarouselItem item={item} index={index} scrollX={scrollX} renderItem={renderItem} itemWidth={itemWidth} spacing={spacing} /> )} /> );};const styles = StyleSheet.create({ itemContainer: { justifyContent: "center", alignItems: "center", }, contentWrapper: { overflow: "hidden", borderRadius: 20, }, blurOverlay: { borderRadius: 20, overflow: "hidden", },});export { CinematicCarousel, CinematicCarouselItemProps, CinematicCarouselProps,};Usage
import React from "react";import { StyleSheet, View, Text, Image, Dimensions } from "react-native";import { SafeAreaView } from "react-native-safe-area-context";import MaskedView from "@react-native-masked-view/masked-view";import BlurView from "@sbaiahmed1/react-native-blur";import { CinematicCarousel } from "@/components/molecules/cinematic-carousel";const SCREEN_WIDTH: number = Dimensions.get("window").width;interface Item { id: string; title: string; color: string; image?: string;}const DATA: Item[] = [ { id: "1", title: "First Item", color: "#FF5733", image: "https://images.pexels.com/photos/1470502/pexels-photo-1470502.jpeg", }, { id: "2", title: "Second Item", color: "#33FF57", image: "https://images.pexels.com/photos/1559825/pexels-photo-1559825.jpeg", }, { id: "3", title: "Third Item", color: "#3357FF", image: "https://images.pexels.com/photos/2377432/pexels-photo-2377432.jpeg", }, { id: "4", title: "Fourth Item", color: "#F333FF", image: "https://images.pexels.com/photos/753339/pexels-photo-753339.jpeg", },];const WIDTH = 300;const HEIGHT = 200;const App = () => { const renderItem: React.FC<any> = ({ item, index, }: { item: Item; index: number; }): React.JSX.Element => { return ( <> {/* @ts-ignore */} <View style={{ width: WIDTH, height: HEIGHT, overflow: "hidden", }} > <Image style={{ width: "100%", height: "100%", }} source={{ uri: item.image, }} /> </View> </> ); }; return ( <SafeAreaView style={styles.container}> <CinematicCarousel data={[...DATA]} renderItem={renderItem} spacing={20} /> </SafeAreaView> );};const styles = StyleSheet.create({ container: { flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "#000", },});export default App;Props
CinematicCarouselItemProps
React Native Reanimated
React Native Blur
