React Native: Carousels with Horizontal Scroll Views

How to create fluid and responsive carousels with native components

Ross Bulat
Jan 27 · 11 min read

ScrollView: One component to rule them all?

ScrollView is one of the most fundamental components of a React Native app, that can act as a standalone component as well as a dependency for a range of other more complex components.

It is impressive thinking about its wide-ranging use-cases. It can be used as a standalone vertical scrolling solution, automatically making your content suitable for any screen height. Other more complex components, like SectionList and FlatList, rely on ScrollView and its props, whereas community driven components like KeyboardAwareScrollView expand upon ScrollView’s functionality to solve complicated UX problems with the virtual keyboard.

And these use cases just entail vertical scrolling. ScrollView also supports horizontal scrolling and pinch to zoom — that tracks the current magnification with the zoomScale prop, along with others to limit its minimum, maximum and zoom behaviour. This makes ScrollView ideal for panning around maps-based applications, or apps with large canvases like boardgames.

The particular use-case this article focusses on, however, is that of a horizontal-only scrollable carousel component that allows the user to swipe through a range of items (or intervals). This is a very common feature in apps today, being utilised to display and structure a range of data:

Common use cases carousels are used in apps

We’ll explore a range of props and event handlers ScrollView offers to make this happen, as well as custom logic for handling carousel state. The demo project referenced in this article is also available on Github, where two styles of carousel are showcased.


How Carousels with ScrollView Work

Utilising ScrollView to create carousels works extremely well, maintaining a native and fluid feel to your app, while keeping the doors open for customisability of the look and behaviour of the carousel itself.

First and foremost, a ScrollView component must be configured to horizontally structure content rather than vertically. By setting the horizontal prop to true, the ScrollView will automatically arrange its child components in columns rather than rows.

This sets us up nicely to endeavour into other props to optimise the behaviour of the view (more on props in the next section).

<Carousel /> Component Structure

In order to visualise how the ScrollView component is used with other components to form a carousel, consider the following illustration:

There is a 3 component foundational setup here:

  • The <ScrollView /> is housed within a <Container /> component that defines the visible area of the carousel. This is where borders, background colours, etc, can be styled, giving some context to the scrollable area.
  • The <ScrollView /> itself. It is important to mention at this stage that the <ScrollView /> itself needs a pre-defined width in its contentContainerStyle prop. Without the width pre-defined, we would not be able to scroll through the ScrollView — we’ll explore some code further down to calculate the width based on how many intervals the carousel holds, as well as snapping to the beginning of each interval when scrolling.

Height does not matter so much — it can be set as a fixed value or be automatic based on the carousel content, depending on your design.

  • Each <Item />, representing one item of the carousel. This is where your displayable content will be housed, such as a row of stats, an app introduction slide, etc. Depending on your design, one <Item /> may span 100% of the <Container /> width, or it may span a half or even a third of the viewable area — this will be considered in the code further down.

A <Item /> is not tied to a carousel interval E.g. one item per swipe. There could be multiple items tied into one interval, such as displaying multiple statistics or graphs in one view. This is carousels should be designed, and is considered in the demos further down.

The above represents a basic component setup to get the carousel working, but is by no means a complete solution.

Based on scroll events, however, we can introduce more UX to compliment the carousel state such as bullet points fixed below the carousel representing each interval with the active interval bullet point being a darker shade. These additional UX components are often placed outside the <ScrollView /> itself, not being subject to scroll events or swipe-able content:

// carousel component hierarchy pseudo code<Container>
<ScrollView>
<Item />
<Item />
<Item />
--- next interval
<Item />
<Item />
<Item />
</ScrollView>
<Bullets />
<Container />

Notice where the next interval is — or the point that will snap to the start of the carousel to separate its stages, that is calculated separately from the Item widths.

Let’s dive into some of the critical props of ScrollView next, before utilising some of the event handlers to calculate vital carousel information.


ScrollView Props and Scroll Events

There are a couple of key props at our disposal to configure a ScrollView to be a paginated, horizontal scroller.

Examine the following example that highlights key props:

// props for a horizontal, paginated ScrollView<ScrollView
horizontal={true}
contentContainerStyle={{ width: `${100 * intervals}%` }}
showsHorizontalScrollIndicator={false}
scrollEventThrottle={200}
decelerationRate="fast"
pagingEnabled
>
{/* Items */}
</ScrollView>

In the above code we are assuming that each <Item /> takes up 100% width of the carousel area, therefore the width of the ScrollView will be 100% multiplied by the amount of items.

The top two props have already been covered, aligning horizontal content within the Scroll View, as well as defining its width in contentContainerStyle. Breaking down the others:

  • The showHorizontalScrollIndicator set to false hides the scroll bar — the horizontal bar that shows up as the user is scrolling to give context to the current scroll position. This bar is not too necessary in a carousel environment, and can be replaced with the aforementioned bullet point mechanism instead.
  • scrollEventThrottle will become important further down for optimising scroll event performance, setting a delay between scroll events being fired. For carousels at least, it is not required to maintain an accurate scroll position — the data derived from these scroll events will determine which interval the carousel is showing only, which can be calculated with some latency without having an impact on user experience.
  • The pagingEnabled prop acts as a snapping shortcut, that stops the Scroll View at multiples of its size when scrolling. This is what effectively separates the scroll view into sections, intelligently snapping the scroll position to the nearest threshold as the user swipes.
  • decelerationRate defines the speed at which the scroll stops after the user lifts their finger. It is recommended to use fast for carousel purposes, shortening the time it takes to transition to the interval in question’s snap position.

Other snapping props of ScrollView

The pagingEnabled prop is a very useful shortcut for defining the interval logic for the Scroll View, but it is worth noting that there are a number of other props that can refine this pagination, or snapping, behaviour.

For example, pagingEnabled can also be achieved with the snapToStart, snapToOffsets, snapToInterval and snapToAlignment props, that can be used together to define custom Scroll View pagination. snapToInterval is also a shortcut in place of defining an array of numbers with snapToOffsets.

snapToStart and snapToOffsets={[0]} achieve the same result.

snapToAlignment is also an interesting prop, that snaps the view to the start, end, or center of a snapping interval. Think of a scenario where multiple Scroll View <Item />’s are on show simultaneously. Setting snapToAlignment to center will automatically snap the active interval to be at the center of the scroll area, consequently giving precedence to the current item being at the center of the view. snapToAlignment can be used with snap props and pagingEnabled.

It is worth studying and experimenting with these props to get a more unique feel to a carousel, if you need to deliver a bespoke carousel experience. For the examples discussed in this article, pagingEnabled alone works very well and keeps syntax to a minimum.

With ScrollView configuration now understood, lets next implement a <Carousel /> component, and introduce scroll events that will aid in carousel state and further UX additions.


Carousel Implementation with ScrollView

In this section we will walk though creating a fully functional <Carousel /> component, separating the implementation into two steps before importing and using it.

Within the Carousel component, ScrollView events will be used to trigger component state updates and initialisation in the following order:

  • Step 1: Initialising the carousel within the onContentSizeChange event, that will calculate how many intervals (carousel “pages”) there are, as well as its width.
  • Step 2: Calculating the currently active interval with the onScroll event, and reflecting that in surrounding UX.

A gist will be presented for the first step, and the final project with the full solution will be linked at the end of the article. To look at this project now, it is available hereon Github. Here is a screencast of the demo, showcasing two styles of carousel:

Both these carousels are derived from the same <Carousel /> component, but render different components for their items, based on a style prop we will be passing into it.

Importing Carousel component and its props

To understand the <Carousel /> component, let’s firstly examine how we want to embed the component within JSX — and what props it requires:

// importing and using `Carousel`import Carousel from './Carousel';export const App = () => ( 
<View style={styles.container}>
<Carousel
style="slides"
itemsPerInterval={1}
items={[{
title: 'Welcome, swipe to continue.',
}, {
title: 'About feature X.',
}, {
title: 'About feature Y.',
}]}
/>
</View>
);

As we can see from above, <Carousel /> supports 3 props, that each play a role in configuring the function and content of the carousel:

  • style defines what type of <item /> component we will use for each item displayed in the carousel. In the demo project we have two components that represent a carousel item — Stat and Slide. Within the carousel itself is a switch statement that renders the component corresponding to the style prop, acting as a simple mechanism to change the look or type of carousel to render.
  • itemsPerInterval allows us to configure how many of the <Item /> component to display on one swipe-able area of the carousel — or how many items are displayed at one time. The value passed in here is used within the component to determine how many swipe-able sections the carousel holds. For the slides style, only 1 item per interval is required. For the stats style however, 3 items per interval is required, with each Stat consuming 33% width.
  • items is an array of objects consisting of the data of each carousel item. The structure of the objects need to be consistent and conform to the <Item /> component‘s data requirements. A Slide component here only requires a title, keeping the item requirements simple.

These props are dealt with at the top of Carousel:

// extracting Carousel propsconst { items, style } = props;
const itemsPerInterval = props.itemsPerInterval === undefined
? 1
: props.itemsPerInterval;

From here we can initialise the carousel.

Carousel initialisation

The initialisation of the carousel is carried out in one of its ScrollView event handlers, onContentSizeChange. This event is actually fired when the Scroll View first renders, allowing us to embed initialisation logic:

// calling init() within `onContentSizeChange`...
return (
<View style={styles.container}>
<ScrollView
...
onContentSizeChange={(w, h) => init(w)}
>
...
</ScrollView>
</View>
)

In the event your Scroll View changes size based on a responsive design, onContentSizeChange will fire again, and keep the carousel dimensions in sync with init().

onContentSizeChange gives us the width and height of the carousel as callback arguments, the width of which is then passed into Carousel’s init() method. init() is responsible for defining the carousel width, along with how many intervals will be present:

// init() implementationconst [intervals, setIntervals] = React.useState(1);
const [width, setWidth] = React.useState(0);
const init = (width: number) => {

// initialise width
setWidth(width);
// get total items present
const totalItems = items.length;
// initialise total intervals
setIntervals(Math.ceil(totalItems / itemsPerInterval));
}

By utilising useState hooks and a simple calculation to determine how many intervals are needed based on how many items the carousel holds, the carousel now has context about its width and snapping intervals required for it to function.

The following Gist outlines the <Carousel /> component at this initialisation stage:

Introducing current interval calculation

To calculate the current carousel interval (also thought of as the carousel “page”), a bit more logic needs to be introduced into the <Carousel /> component. Firstly, an additional useState hook to store the current interval, as a default value of 1:

const [interval, setInterval] = React.useState(1);

In order to calculate the active interval, we rely on ScrollView’s onScroll event handler. When it fires we will call another method, getInterval(), to determine the current interval based on the ScrollView’s current offset:

// defining onScroll event and getInterval()const getInterval = (offset: any) => {
for (let i = 1; i <= intervals; i++) {
if (offset < (width / intervals) * i) {
return i;
}
if (i == intervals) {
return i;
}
}
}
...<ScrollView
...
onScroll={data => {
setInterval(getInterval(data.nativeEvent.contentOffset.x));
}}
scrollEventThrottle={200}
>
...
</ScrollView>

getInterval() is simply a for loop with a couple of conditional statements inside it, checking if the current scroll offset falls between certain interval thresholds. The scrollEventThrottle prop limits the scroll event to fire every 200 milliseconds, not to put too much of a burden on performance.

Adding supplemental UX

With the knowledge of knowing what interval the carousel is now on, we can now introduce UX such as bullet points as visual aids to the currently selected interval.

To construct the bullet points dynamically, another for loop can be used to build the correct amount of bullets based on the number of intervals:

// constructing bullet pointslet bullets = [];
for (let i = 1; i <= intervals; i++) {
bullets.push(
<Text
key={i}
style={{
...styles.bullet,
opacity: interval=== i ? 0.5 : 0.1
}}
>
&bull;
</Text>
);
}

Each bullet point is wrapped within a native Text component, with the currently selected interval having a less intense opacity. Once constructed, the bullets array can simply be embedded in JSX:

// inserting bullets after ScrollView

...
</ScrollView>
<View style={styles.bullets}>
{bullets}
</View>
</View>
)

The full implementation of Carousel can be found here.


In Summary

This wraps up the base implementation of the horizontal carousel, giving a solid foundation with the hope developers can build upon to achieve more unique styles and behaviours.

This article has demonstrated how to create native feeling carousels with ScrollView, and how to build the surrounding carousel logic around the component. We have also looked into how to structure a standalone <Carousel /> component that supports various stage styles with particular components, such as Stat and Slide.

The demo project ultimately attempts to abstract key logic and styling for the developer to decide, not being tied down to any particular size or layout of a carousel item.

The project can be viewed or cloned here on Github, with the playable demo showcasing both the statistics and slideshow styled carousels.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store