Building a Gallery Carousel in React Native using Reanimated — I

Karthik Balasubramanian
Timeless
5 min readAug 30, 2023

--

Well, hello everyone!

In this blog, we are going to build a simple Carousel Component in React Native.

Inspired by one of the Tweets, my thought went like, “Can it be done in React Native?”. Well, I think many other developers would have done it. This blog is just my take on how I build it.

Take a look at the Interaction we will be building.

Inspiration Tweet

Now, seen that, let's get coding.

Blog Cover

React Native Project Setup

You can use React Native’s built-in command line interface to generate a new project.

npx react-native@latest init AwesomeProject

The packages needed to get the Interaction done are only React Native Reanimated and Gesture Handler libraries.

For styling, I will use the twrnc package, an API for Tailwind CSS with React Native.

npm install --save twrnc

As we will be using SVG in our project, we need to install them as well

yarn add react-native-svg
// And Linking
cd ios && pod install

Furthermore, install react-native-reanimated and react-native-gesture-handler libraries.

yarn add react-native-reanimated react-native-gesture-handler

Get detailed instructions for reanimated here and gesture handler here.

That’s it for dependencies.

Let us run our React Native app now.

// For iOS Simulator
npx react-native run-ios
// For Android, make sure you have an Android Emulator running
npx react-native run-android

That’s all for setting up. 🎉

Setting up with required Assets, Constants, and Data

import { ArrowLeft, ArrowRight } from "../icons";

const CARD_WIDTH = 300;
const SCREEN_WIDTH = Dimensions.get("screen").width;

const carouselItems = [
{
image:
"https://images.unsplash.com/photo-1678436748951-ef6d381e7a25?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHx0b3BpYy1mZWVkfDN8YWV1NnJMLWo2ZXd8fGVufDB8fHx8fA%3D%3D",
ar: 0.7,
},
{
image:
"https://images.unsplash.com/photo-1680813977591-5518e78445a0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
ar: 0.67,
},
{
image:
"https://images.unsplash.com/photo-1679508056887-5c76269dad54?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
ar: 0.8,
},
{
image:
"https://images.unsplash.com/photo-1681243303374-72d01f749dfa?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHx0b3BpYy1mZWVkfDczfGFldTZyTC1qNmV3fHxlbnwwfHx8fHw%3D",
ar: 0.68,
},
{
image:
"https://images.unsplash.com/photo-1675185741953-18b60234cb79?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
ar: 0.67,
},
{
image:
"https://images.unsplash.com/photo-1685725083464-26cab8f2da1e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
ar: 0.67,
},
];

We would set the Width of a single card to 300px, you can change this based on your requirements. And create a variable SCREEN_WIDTH set it to the device width.

We need a set of images with its Aspect Ratio to place it properly inside our card and is stretched or shrunk without getting a deformed image.

The icons are SVG icons taken from https://heroicons.com/

import Svg, { Path } from "react-native-svg";

type ArrowLeftProps = {
stroke?: string;
};

export const ArrowLeft = ({ stroke = "black" }: ArrowLeftProps) => {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke={stroke}
height={24}
width={24}
>
<Path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75"
/>
</Svg>
);
};

export const ArrowRight = ({ stroke = "black" }: ArrowRightProps) => {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke={stroke}
height={24}
width={24}
>
<Path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"
/>
</Svg>
);
};

Creating a Simple ScrollView with Images

Before creating the ScrollView, let us define some variables which will be useful to manage behavior and state for our Interaction.

const scrollXOffset = useSharedValue(0);

const [isFirstCard, setIsFirstCard] = useState(scrollXOffset.value === 0);
const [isLastCard, setIsLastCard] = useState(false);
const [scrollViewWidth, setScrollViewWidth] = useState(0);

const scrollRef = useRef<Animated.ScrollView>(null);

The first is the scrollXOffset SharedValue variable, from the Reanimated library, created using useSharedValuethe ⁣ hook. It is used to track the current horizontal scroll position.

The next two are the React useState variables, isFirstCard and isLastCard used to check if the scroll position is at first card or has reached the end of the carousel.

The isFirstCard is initialized based on the initial value of scrollXOffset.value, which when is 0 is set to true.

Next is the scrollViewWidth, which is used to store the width of the entire ScrollView component. This is used to calculate the final position of the Carousel.

Finally, scrollRef is a variable to hold the reference to the Animated.ScrollView component. It is used to scroll the view programmatically.

Coding the Component Render

Our component render will look something like this:

return (
<SafeAreaView style={tailwind.style("flex-1 justify-center bg-white")}>
<Animated.View style={tailwind.style("overflow-visible")}>
<Animated.ScrollView
ref={scrollRef}
horizontal
onScroll={scrollHandler}
showsHorizontalScrollIndicator={false}
snapToInterval={CARD_WIDTH}
scrollEventThrottle={16}
decelerationRate={0}
style={tailwind.style("overflow-visible")}
contentContainerStyle={tailwind.style("overflow-visible pl-4 pr-4")}
onContentSizeChange={handleContentSizeChange}
>
{carouselItems.map((item, index) => (
<GalleryCarouselItem
key={index}
{...{
item,
index,
scrollXOffset,
handleCarouselItemPress,
scrollViewWidth,
}}
/>
))}
</Animated.ScrollView>
<Animated.View style={tailwind.style("px-4 flex flex-row pt-5")}>
<Pressable
disabled={isFirstCard}
onPress={goToPrevious}
style={({ pressed }) =>
tailwind.style(
pressed ? "bg-gray-100 rounded-xl" : "",
"mr-2 p-2",
)
}
>
<Animated.View style={tailwind.style("")}>
<ArrowLeft
stroke={isFirstCard ? tailwind.color("bg-gray-400") : "black"}
/>
</Animated.View>
</Pressable>
<Pressable
disabled={isLastCard}
onPress={goToNext}
style={({ pressed }) =>
tailwind.style(
pressed ? "bg-gray-100 rounded-xl" : "",
"mr-2 p-2",
)
}
>
<Animated.View style={tailwind.style("")}>
<ArrowRight
stroke={isLastCard ? tailwind.color("bg-gray-400") : "black"}
/>
</Animated.View>
</Pressable>
</Animated.View>
</Animated.View>
</SafeAreaView>
);

The above code implements a simple Horizontal Carousel with Navigation controls, the ArrowLeft (previous) and ArrowRight (next).

We wrap our whole component inside <SafeAreaView /> which ensures that the content displayed are withing safe areas of the screen, accounting for notches.

Next comes an outermost <Animated.View> which wrap the entire carousel(ScrollView) with the navigation controls.

The Animated.ScrollView is the core component of our horizontal carousel. First we use the ref prop, set it to scrollRef for making manual operations on the View.

The ScrollView renders a set of carousel items, using the map function. Each array item is rendered using the <GalleryCarouselItem />, and we pass required props to the component.

Next comes a onScroll which is used to handle scroll events, we create a scrollHandler using useAnimatedScrollHandler() from the Reanimated library.

To set the size of the full scroll view, we use the onContentSizeChange prop, create a function and set the value.

const handleContentSizeChange = (width: number, _height: number) => {
setScrollViewWidth(width);
};

We will have to set the snap intervals for the Carousel, which is set to the CARD_WIDTH value, set the scrollEventThrottle to 16, and decelerationRate to 0.

Next is the pair of Navigation Controls, including the ArrowLeft and ArrowRight icons, which are used for navigating the carousel.

The two buttons are implemented using the Pressable component from React Native, and are conditionally disabled based on the two variables we have declared before, isFirstCard and isLastCard variables.

When interacted, there are a couple of function definitions which are called, the goToPrevious and goToNext functions.

With all this in place, we would have something like this:

Initial Interaction

You can see the navigation controls gets disabled based on the position of the carousel.

That is it for this blog, in the next blog we will see how to tie the scroll to the Card Component, animate the style based on the scrollXOffset and the definition of Navigation Control functions.

This is Karthik from Timeless

See you guys in the second part of my blog.

Thanks for reading!

--

--