Custom Scrolling Carousel in React Native

React Native’s ScrollView component does a pretty good job serving up Image Carousels, but there’s no built in scroll indicator for carousels.

I recently was asked to implement a carousel with a custom scroll indicator. The result was to look like this:

The carousel itself is easy — take a scrollview, fill it with images, and add the following properties:

<Scrollview
horizontal //scrolling left to right instead of top to bottom
showsHorizontalScrollIndicator={false} //hides native scrollbar
scrollEventThrottle={10} //how often we update the position of the indicator bar
pagingEnabled //scrolls from one image to the next, instead of allowing any value inbetween
/>

And setting up the bars is pretty simple as well, each one is a view that represents the background / inactive color, and contains an absolutely positioned view which sits above that, with an active color.

<View
style={{width: [width], height: 2, backgroundColor: [inactiveColor], overflow: hidden}}
>
<View
style={{position: 'absolute', top: 0, left: 0, width: [width], backgroundColor: [activeColor]}}
/>
</View>

But of course, only one of the bars should be active at a time. The other bars should have their active colors hidden. And we want the active bar to animate into place.

Since we don’t have the active scroll item index from the scroll view per se, we need to derive it from the scroll position. We get that with the following code, attached to the ScrollView:

onScroll={            
Animated.event(
[{ nativeEvent: { contentOffset: { x: this.animVal } } }]
)
}

We intercept the scroll value, and set our animated value to the scroll view’s content offset, for use in animating our indicator bars. We will translate the X position of the scroll bar, from -[barWidth] when the bar is inactive, to [0] when it is active, to [barWidth], when it is on its way out.

To do that, the bars will interpolate the scroll value, but each bar will need its own interpolation, so that it can react to the scroll value only when it is in an appropriate range. Here’s where that happens:

const scrollBarVal = this.animVal.interpolate({        
inputRange: [deviceWidth * (i - 1), deviceWidth * (i + 1)],
outputRange: [-this.itemWidth, this.itemWidth],
extrapolate: 'clamp',
})

Each bar has an index [0,1,2,etc]. If our device width is 375, then our first bar should be at position 0 when the scrollview is at 0, and 375 (inactive, when the scrollview is at 375. And the second bar should be at -375 when the scrollview is at 0, 0 when the scrollview is at 375, and 375 when the scrollview is at 750. So our input range for each bar’s interpolation is:

[deviceWidth * (i - 1), deviceWidth * (i + 1)]

and our output range for each bar is the same, -375 to 375, or:

[-this.itemWidth, this.itemWidth]

The rest of the code is pretty straightforward — it just deals with the spacing of the bars, from each other, obtaining the device width, and looping through our image array to actually render the components. Here is the code in its entirety, and below that is a link to @exponent’s snack utility, where you can preview the carousel, or test it out on your own device.