(React Native) Create a Horizontal Snap ScrollView

Kenta Kodashima
Nerd For Tech
Published in
5 min readJan 14, 2020
The UI of what we are building

In React Native, you can create a horizontal snap ScrollView using just simple calculations. In this article, I will show you how easy it can be. The final source code is available on my GitHub.

Prerequisites

  • I will be using Typescript in this article
  • The project is generated using pure React Native
  • Basic knowledge of pure React Native development

My Environment

  • React Native: 0.61.5
  • macOS Catalina: Version 10.15.2 (19C57)
  • Typescript: 3.7.4

1. Project Setup

Create a project with the following steps.

// Generate a project
react-native init HorizontalSnapScrollViewExample
// Enter into the project directory
cd HorizontalSnapScrollViewExample
// Add typescript
npm i -D typescript react-native-typescript-transformer
// Add eslint with typescript plugin
npm i -D eslint
@typescript-eslint/parser @typescript-eslint/eslint-plugin
// Add types
npm i -D @types/react @types/react-native
// Optionally, you can add prettier
npm i -D prettier eslint-config-prettier eslint-plugin-prettier

Then, modify the .eslinttrc.js file something like below to configure Typescript stuff.

// .eslinttrc.jsmodule.exports = {
root: true,
parser: '
@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018, // Allow to parse modern ECMAScript features
sourceType: 'module', // Allow to use imports
ecmaFeatures: {
jsx: true,
},
},
plugins: ['
@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:
@typescript-eslint/recommended',
'plugin:react/recommended',
'prettier',
'prettier/@typescript-eslint',
],
rules: {
semi: 0,
'ordered-imports': 0,
'object-literal-sort-keys': 0,
'member-ordering': 0,
'jsx-no-lambda': 0,
'jsx-boolean-value': 0,
'no-console': 0,
'no-empty-interface': 0,
'interface-name': [0, 'always-prefix'],
'
@typescript-eslint/no-unused-vars': 0,
'
@typescript-eslint/camelcase': 0,
'
@typescript-eslint/no-explicit-any': 0,
'
@typescript-eslint/explicit-function-return-type': [
'warn',
{
'allowExpressions': 1,
'allowTypedFunctionExpressions': 1
}
]
},
env: {
es6: true,
node: true,
}
}

Finally, rename the App.js and index.js files to App.tsx and index.ts. Also, create the src folder at the root directory and move the App.tsx file in the src directory. After moving the App.tsx file, don’t forget to also modify the directory in the index.ts file.

Also, modify the App.tsx as below to prepare for the rest of this tutorial.

import React, { Component } from 'react'
import {
SafeAreaView,
StyleSheet,
ScrollView,
View,
Text,
StatusBar,
Dimensions,
Platform

} from 'react-native'
class App extends Component {
render() {
return (
<React.Fragment>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView>
// The contents would be here
</ScrollView>
</SafeAreaView>
</React.Fragment>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
export default App

That’s everything for the setup.

2. Prepare the view to display inside ScrollView

First, we need to prepare the views to display inside ScrollView. Modify App.tsx as bellow.

import React, { Component } from 'react'
import {
...
} from 'react-native'
// ==== 1 ====
const CARD_WIDTH = Dimensions.get('window').width * 0.8
const CARD_HEIGHT = Dimensions.get('window').height * 0.7
// ==== 2 ====
type CardType = {
name: string
}
// ==== 3 ====
const cards = [
{ name: 'Card 1' },
{ name: 'Card 2' },
{ name: 'Card 3' },
{ name: 'Card 4' },
{ name: 'Card 5' },
{ name: 'Card 6' },
{ name: 'Card 7' },
{ name: 'Card 8' },
{ name: 'Card 9' },
{ name: 'Card 10' }
]
class App extends Component {
// ==== 5 ====
_renderViews = (views: CardType[]): JSX.Element[] => {
const { cardStyle } = styles
return views.map(card => {
return (
<View style={cardStyle}>
<Text>
{card.name}
</Text>
</View>
)
})
}
render() {
// ==== 6 ====
const { container } = styles
return (
<React.Fragment>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={container}>
<ScrollView>
{this._renderViews(cards)}
</ScrollView>
</SafeAreaView>
</React.Fragment>
)
}
}
// ==== 4 ====
const styles = StyleSheet.create({
...,
cardStyle: {
width: CARD_WIDTH,
height: CARD_HEIGHT,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'green',
margin: 5,
borderRadius: 15
}

})
export default App

I will explain the changes step by step.

  1. The constants CARD_WIDTH and CARD_HEIGHT
    I calculate the width and height using the ratio. By doing this way, the cards will have the same width and height ratio no matter what devices.
  2. CardType
    A type definition to describe a card object.
  3. The cards constant
    The data to display.
  4. Add cardStyle to the styles constant
    Define the styling for a card.
  5. The _renderViews method
    A method to take an array of objects and render them. I wrote this function to make render() more concise.
  6. Call the _renderViews method
    Call _renderViews() inside ScrollView to render the cards. Don’t forget to pass the cards array and apply the container styling to SafeAreaView .

Now, the app should look like the image below.

The UI of the current app in iOS

3. Configure ScrollView

Since we have something to display now, it’s about time to configure the ScrollView.

First, add the following constant right below the CARD_HEIGHT constant.

const SPACING_FOR_CARD_INSET = Dimensions.get('window').width * 0.1 - 10

SPACING_FOR_CARD_INSET will be used to have some spacing at the start and end of the horizontal ScrollView.

Then, modify the ScrollView as below. I explain the meaning of the props in comments.

<ScrollView
horizontal // Change the direction to horizontal
pagingEnabled // Enable paging
decelerationRate={0} // Disable deceleration
snapToInterval={CARD_WIDTH+10} // Calculate the size for a card including marginLeft and marginRight
snapToAlignment='center' // Snap to the center
contentInset={{ // iOS ONLY
top: 0,
left: SPACING_FOR_CARD_INSET, // Left spacing for the very first card
bottom: 0,
right: SPACING_FOR_CARD_INSET // Right spacing for the very last card
}}
contentContainerStyle={{ // contentInset alternative for Android
paddingHorizontal: Platform.OS === 'android' ? SPACING_FOR_CARD_INSET : 0 // Horizontal spacing before and after the ScrollView
}}

>
{this._renderCardList(cards)}
</ScrollView>

Notice contentInset is only applicable to iOS. In order to position the cards correctly in Android, I use contentContainerStyle as an alternative. The contentInset prop will automatically be set to ‘none’ in Android.

Note:
You might think that we can use contentContainerStyle for both iOS and Android. However, it did not work for me. When I tried to position the cards using this prop in iOS, it gave me the wrong positions for some reason. Therefore, I just stick with contentInset in iOS.

Now, you should have a nice horizontal snap ScrollView as the image below.

The final app (Left: iOS, Right: Android)

That’s everything for this article! It was easy enough, eh?

The final source code is available here.

References:

--

--

Kenta Kodashima
Nerd For Tech

I'm a Software Engineer based in Vancouver. Personal Interests: Books, Philosophy, Piano, Movies, Music, Studio Ghibli.