Vertical Flow Carousel

A vertical snapping carousel

Last updated on

Edit on GitHub

Manual

Install the following dependencies:

npm install react-native-reanimated expo-blur

Copy and paste the following code into your project. component/molecules/vertical-flow-carousel

import { BlurView } from "expo-blur";import React from "react";import { Dimensions, StyleSheet, View } from "react-native";import Animated, {  Extrapolation,  interpolate,  SharedValue,  useAnimatedScrollHandler,  useAnimatedStyle,  useSharedValue,} from "react-native-reanimated";import type { AnimatedItemProps, VerticalCarouselProps } from "./types";const { height: SCREEN_HEIGHT } = Dimensions.get("window");function AnimatedItem<T>({  item,  index,  scrollY,  itemHeight,  spacing,  rotationAngle,  scaleInactive,  opacityInactive,  showBlur,  blurIntensity,  children,  totalItems,}: AnimatedItemProps<T>) {  const animatedStyle = useAnimatedStyle(() => {    const inputRange = [      (index - 1) * (itemHeight + spacing),      index * (itemHeight + spacing),      (index + 1) * (itemHeight + spacing),    ];    const scale = interpolate(      scrollY.value,      inputRange,      [scaleInactive, 1, scaleInactive],      Extrapolation.CLAMP,    );    const opacity = interpolate(      scrollY.value,      inputRange,      [opacityInactive, 1, opacityInactive],      Extrapolation.CLAMP,    );    const rotateZ = interpolate(      scrollY.value,      inputRange,      [rotationAngle, 0, -rotationAngle],      Extrapolation.CLAMP,    );    return {      transform: [        { scale },        { rotateZ: `${rotateZ}deg` },        { perspective: 1000 },      ],      opacity,    };  });  const blurOpacity = useAnimatedStyle(() => {    const inputRange = [      (index - 1) * (itemHeight + spacing),      index * (itemHeight + spacing),      (index + 1) * (itemHeight + spacing),    ];    const blur = interpolate(      scrollY.value,      inputRange,      [1, 0, 1],      Extrapolation.CLAMP,    );    return {      opacity: blur,    };  });  return (    <Animated.View      style={[        styles.itemContainer,        animatedStyle,        {          marginBottom: index === totalItems - 1 ? 400 : spacing,        },      ]}    >      <View style={styles.itemContent}>        {children}        {showBlur && (          <Animated.View            style={[              StyleSheet.absoluteFill,              blurOpacity,              { overflow: "hidden" },            ]}          >            <BlurView              intensity={blurIntensity}              style={[                StyleSheet.absoluteFill,                {                  overflow: "hidden",                },              ]}              tint="dark"            />          </Animated.View>        )}      </View>    </Animated.View>  );}export default function VerticalFlowCarousel<T>({  data,  renderItem,  itemHeight = 120,  spacing = 50,  containerStyle,  contentContainerStyle,  showBlur = true,  blurIntensity = 16,  rotationAngle = 12,  scaleInactive = 0.85,  opacityInactive = 0.5,  snapEnabled = true,}: VerticalCarouselProps<T>) {  const scrollY = useSharedValue(0);  const scrollHandler = useAnimatedScrollHandler({    onScroll: (event) => {      scrollY.value = event.contentOffset.y;    },  });  return (    <View style={[styles.container, containerStyle]}>      <Animated.ScrollView        onScroll={scrollHandler}        scrollEventThrottle={16}        snapToInterval={snapEnabled ? itemHeight + spacing : undefined}        decelerationRate="fast"        showsVerticalScrollIndicator={false}        contentContainerStyle={[styles.scrollContent, contentContainerStyle]}      >        {data.map((item, index) => (          <AnimatedItem            key={index}            item={item}            index={index}            scrollY={scrollY}            itemHeight={itemHeight}            spacing={spacing}            rotationAngle={rotationAngle}            scaleInactive={scaleInactive}            opacityInactive={opacityInactive}            showBlur={showBlur}            blurIntensity={blurIntensity}            totalItems={data.length}          >            {renderItem(item, index)}          </AnimatedItem>        ))}      </Animated.ScrollView>    </View>  );}const styles = StyleSheet.create({  container: {    flex: 1,  },  scrollContent: {    // paddingHorizontal: 30,  },  itemContainer: {    width: "100%",  },  itemContent: {    width: "100%",  },});

Usage

import { View, Text, StyleSheet, Image, Dimensions } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";import { StatusBar } from "expo-status-bar";import { useFonts } from "expo-font";import VerticalFlowCarousel from "@/components/molecules/vertical-flow-carousel";const { width } = Dimensions.get("window");const DATA = [  {    id: "1",    title: "Mountain Peak",    location: "Switzerland",    image: "https://picsum.photos/id/29/800/600",  },  {    id: "2",    title: "Ocean Waves",    location: "Maldives",    image: "https://picsum.photos/id/28/800/600",  },  {    id: "3",    title: "Forest Trail",    location: "Canada",    image: "https://picsum.photos/id/15/800/600",  },  {    id: "4",    title: "Desert Dunes",    location: "Morocco",    image: "https://picsum.photos/id/33/800/600",  },];export default function App() {  const [fontLoaded] = useFonts({    SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"),    HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"),  });  return (    <GestureHandlerRootView style={styles.container}>      <StatusBar style="light" />      <VerticalFlowCarousel        data={DATA}        itemHeight={210}        rotationAngle={12}        opacityInactive={0.5}        scaleInactive={0.8}        showBlur        snapEnabled        blurIntensity={20}        contentContainerStyle={styles.carousel}        renderItem={(item) => (          <View style={styles.card}>            <Image source={{ uri: item.image }} style={styles.image} />            <View style={styles.overlay}>              <Text                style={[                  styles.title,                  fontLoaded && { fontFamily: "HelveticaNowDisplay" },                ]}              >                {item.title}              </Text>              <Text                style={[                  styles.location,                  fontLoaded && { fontFamily: "SfProRounded" },                ]}              >                {item.location}              </Text>            </View>          </View>        )}      />    </GestureHandlerRootView>  );}const styles = StyleSheet.create({  container: {    flex: 1,    backgroundColor: "#000",  },  carousel: {    paddingTop: 80,    paddingHorizontal: 20,    paddingBottom: 40,  },  card: {    width: width - 40,    height: 280,    borderRadius: 20,    overflow: "hidden",  },  image: {    width: "100%",    height: "100%",  },  overlay: {    position: "absolute",    bottom: 0,    left: 0,    right: 0,    padding: 20,    backgroundColor: "rgba(0,0,0,0.4)",  },  title: {    fontSize: 22,    color: "#fff",    marginBottom: 4,  },  location: {    fontSize: 14,    color: "rgba(255,255,255,0.7)",  },});

Props

AnimatedItemProps

React Native Reanimated
Expo Blur