Atom Lab logo

How to build a star rating component with React Native ⭐️⭐️⭐️⭐️⭐️

Tired of reinventing the wheel?

I'm building a React Native component library - built with Tailwind CSS - designed to help developers build awesome apps in record time.

Find out more

Introduction

Building a movie listings app? Or an airbnb style real estate rental app? There are many situations where you’ll need to add user ratings to a React Native project. When this happens you’ll likely want to build a simple star rating component.

A note on icons

For this tutorial we’re using Expo Vector Icons for our star icons, which ships automatically with projects created via Expo’s CLI. If you’re using a non-Expo React Native project, you can use React Native Vector Icons as a direct replacement.

Of course, feel free to use whichever icon library you like instead! Those are just the ones I’ll be using in this tutorial.

Scaffolding our star rating component

Let’s set up the basic structure of our app. First we’ll create a SafeAreaView to deal with device notches, and a View that will act as our container:

import { StyleSheet, View, SafeAreaView } from 'react-native'; export default function App() { return ( <SafeAreaView style={{ flex: 1 }}> <View style={styles.container}></View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', padding: 20, }, });

Now we’ll add our five stars using the MaterialIcons component from expo vector icons, as well as a simple heading:

import { StyleSheet, Text, View, SafeAreaView } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; export default function App() { return ( <SafeAreaView style={{ flex: 1 }}> <View style={styles.container}> <Text style={styles.heading}>Tap to rate</Text> <View style={styles.stars}> <MaterialIcons name="star-border" size={32} style={styles.starUnselected} /> <MaterialIcons name="star-border" size={32} style={styles.starUnselected} /> <MaterialIcons name="star-border" size={32} style={styles.starUnselected} /> <MaterialIcons name="star-border" size={32} style={styles.starUnselected} /> <MaterialIcons name="star-border" size={32} style={styles.starUnselected} /> </View> </View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', padding: 20, }, heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 20, }, stars: { display: 'flex', flexDirection: 'row', }, starUnselected: { color: '#aaa', }, });

Now we have the basic structure of our star rating component:

The scaffolding of our star rating component

Adding star rating with useState hook

Now we need to add a state value that will store our rating to make it interactive. Lets import the useState hook from react and create a starRating state value:

import React, { useState } from 'react'; // export default function App() { const [starRating, setStarRating] = useState(null); // } //

Now for each of our stars we’ll add an onPress function that will set the starRating value. We’ll also set a ternary operator in our heading to display our star rating if selected, and an instruction if not:

import React, { useState } from 'react'; import { StyleSheet, Text, View, SafeAreaView, TouchableOpacity } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; export default function App() { const [starRating, setStarRating] = useState(null); return ( <SafeAreaView style={{ flex: 1 }}> <View style={styles.container}> <Text style={styles.heading}>{starRating ? `${starRating}*` : 'Tap to rate'}</Text> <View style={styles.stars}> <TouchableOpacity onPress={() => setStarRating(1)}> <MaterialIcons name="star-border" size={32} style={styles.starUnselected} /> </TouchableOpacity> <TouchableOpacity onPress={() => setStarRating(2)}> <MaterialIcons name="star-border" size={32} style={styles.starUnselected} /> </TouchableOpacity> <TouchableOpacity onPress={() => setStarRating(3)}> <MaterialIcons name="star-border" size={32} style={styles.starUnselected} /> </TouchableOpacity> <TouchableOpacity onPress={() => setStarRating(4)}> <MaterialIcons name="star-border" size={32} style={styles.starUnselected} /> </TouchableOpacity> <TouchableOpacity onPress={() => setStarRating(5)}> <MaterialIcons name="star-border" size={32} style={styles.starUnselected} /> </TouchableOpacity> </View> </View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', padding: 20, }, heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 20, }, stars: { display: 'flex', flexDirection: 'row', }, starUnselected: { color: '#aaa', }, });
Storing our star rating value with useState

Styling our star rating component

Now we can set a star rating, let’s style our star rating component. For each of our stars we’ll add a ternary operator to the style property that will check if the star’s value is equal or greater than the current starRating value - if it is, it will use the starUnselected class, if it isn’t, it will use the starSelected class which we’ve added below:

import React, { useState } from 'react'; import { StyleSheet, Text, View, SafeAreaView, TouchableOpacity } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; export default function App() { const [starRating, setStarRating] = useState(null); return ( <SafeAreaView style={{ flex: 1 }}> <View style={styles.container}> <Text style={styles.heading}>{starRating ? `${starRating}*` : 'Tap to rate'}</Text> <View style={styles.stars}> <TouchableOpacity onPress={() => setStarRating(1)}> <MaterialIcons name={starRating >= 1 ? 'star' : 'star-border'} size={32} style={starRating >= 1 ? styles.starSelected : styles.starUnselected} /> </TouchableOpacity> <TouchableOpacity onPress={() => setStarRating(2)}> <MaterialIcons name={starRating >= 2 ? 'star' : 'star-border'} size={32} style={starRating >= 2 ? styles.starSelected : styles.starUnselected} /> </TouchableOpacity> <TouchableOpacity onPress={() => setStarRating(3)}> <MaterialIcons name={starRating >= 3 ? 'star' : 'star-border'} size={32} style={starRating >= 3 ? styles.starSelected : styles.starUnselected} /> </TouchableOpacity> <TouchableOpacity onPress={() => setStarRating(4)}> <MaterialIcons name={starRating >= 4 ? 'star' : 'star-border'} size={32} style={starRating >= 4 ? styles.starSelected : styles.starUnselected} /> </TouchableOpacity> <TouchableOpacity onPress={() => setStarRating(5)}> <MaterialIcons name={starRating >= 5 ? 'star' : 'star-border'} size={32} style={starRating >= 5 ? styles.starSelected : styles.starUnselected} /> </TouchableOpacity> </View> </View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', padding: 20, }, heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 20, }, stars: { display: 'flex', flexDirection: 'row', }, starUnselected: { color: '#aaa', }, starSelected: { color: '#ffb300', }, });
Our styled star rating component

Adding animation with the Animated API

We now have a working star rating component, but the actual animation looks a bit barebones. Let’s use the React Native Animated API to give some better visual feedback to the user.

First we need to switch out our TouchableOpacity wrappers to TouchableWithoutFeedback instead:

<TouchableWithoutFeedback onPress={() => setStarRating(1)}> <MaterialIcons name={starRating >= 1 ? 'star' : 'star-border'} size={32} style={starRating >= 1 ? styles.starSelected : styles.starUnselected} /> </TouchableWithoutFeedback>

Now we add onPressIn and onPressOut functions that wlll scale our stars using a transform value:

import React, { useState } from 'react'; import { StyleSheet, Text, View, SafeAreaView, TouchableWithoutFeedback, Animated, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; export default function App() { const [starRating, setStarRating] = useState(null); const animatedButtonScale = new Animated.Value(1); const handlePressIn = () => { Animated.spring(animatedButtonScale, { toValue: 1.5, useNativeDriver: true, speed: 50, bounciness: 4, }).start(); }; const handlePressOut = () => { Animated.spring(animatedButtonScale, { toValue: 1, useNativeDriver: true, speed: 50, bounciness: 4, }).start(); }; const animatedScaleStyle = { transform: [{ scale: animatedButtonScale }], }; return ( <SafeAreaView style={{ flex: 1 }}> <View style={styles.container}> <Text style={styles.heading}>{starRating ? `${starRating}*` : 'Tap to rate'}</Text> <View style={styles.stars}> <TouchableWithoutFeedback onPressIn={handlePressIn} onPressOut={handlePressOut} onPress={() => setStarRating(1)} > <Animated.View style={animatedScaleStyle}> <MaterialIcons name={starRating >= 1 ? 'star' : 'star-border'} size={32} style={starRating >= 1 ? styles.starSelected : styles.starUnselected} /> </Animated.View> </TouchableWithoutFeedback> <TouchableWithoutFeedback onPressIn={handlePressIn} onPressOut={handlePressOut} onPress={() => setStarRating(2)} > <Animated.View style={animatedScaleStyle}> <MaterialIcons name={starRating >= 2 ? 'star' : 'star-border'} size={32} style={starRating >= 2 ? styles.starSelected : styles.starUnselected} /> </Animated.View> </TouchableWithoutFeedback> <TouchableWithoutFeedback onPressIn={handlePressIn} onPressOut={handlePressOut} onPress={() => setStarRating(3)} > <Animated.View style={animatedScaleStyle}> <MaterialIcons name={starRating >= 3 ? 'star' : 'star-border'} size={32} style={starRating >= 3 ? styles.starSelected : styles.starUnselected} /> </Animated.View> </TouchableWithoutFeedback> <TouchableWithoutFeedback onPressIn={handlePressIn} onPressOut={handlePressOut} onPress={() => setStarRating(4)} > <Animated.View style={animatedScaleStyle}> <MaterialIcons name={starRating >= 4 ? 'star' : 'star-border'} size={32} style={starRating >= 4 ? styles.starSelected : styles.starUnselected} /> </Animated.View> </TouchableWithoutFeedback> <TouchableWithoutFeedback onPressIn={handlePressIn} onPressOut={handlePressOut} onPress={() => setStarRating(5)} > <Animated.View style={animatedScaleStyle}> <MaterialIcons name={starRating >= 5 ? 'star' : 'star-border'} size={32} style={starRating >= 5 ? styles.starSelected : styles.starUnselected} /> </Animated.View> </TouchableWithoutFeedback> </View> </View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', padding: 20, }, heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 20, }, stars: { display: 'flex', flexDirection: 'row', }, starUnselected: { color: '#aaa', }, starSelected: { color: '#ffb300', }, });

Here’s our updated star rating component with our improved animation!

Our animated star rating component

Using an array for our star rating options

If you want to make the component a little tidier, you can use an array to store and loop over the star rating values. Another advantage of this is that you can easily change the star rating scale from 5 to whatever you like:

import React, { useState } from 'react'; import { StyleSheet, Text, View, SafeAreaView, TouchableWithoutFeedback, Animated, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; export default function App() { const starRatingOptions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const [starRating, setStarRating] = useState(null); const animatedButtonScale = new Animated.Value(1); const handlePressIn = () => { Animated.spring(animatedButtonScale, { toValue: 1.5, useNativeDriver: true, speed: 50, bounciness: 4, }).start(); }; const handlePressOut = () => { Animated.spring(animatedButtonScale, { toValue: 1, useNativeDriver: true, speed: 50, bounciness: 4, }).start(); }; const animatedScaleStyle = { transform: [{ scale: animatedButtonScale }], }; return ( <SafeAreaView style={{ flex: 1 }}> <View style={styles.container}> <Text style={styles.heading}>{starRating ? `${starRating}*` : 'Tap to rate'}</Text> <View style={styles.stars}> {starRatingOptions.map((option) => ( <TouchableWithoutFeedback onPressIn={() => handlePressIn(option)} onPressOut={() => handlePressOut(option)} onPress={() => setStarRating(option)} key={option} > <Animated.View style={animatedScaleStyle}> <MaterialIcons name={starRating >= option ? 'star' : 'star-border'} size={32} style={starRating >= option ? styles.starSelected : styles.starUnselected} /> </Animated.View> </TouchableWithoutFeedback> ))} </View> </View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', padding: 20, }, heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 20, }, stars: { display: 'flex', flexDirection: 'row', }, starUnselected: { color: '#aaa', }, starSelected: { color: '#ffb300', }, });
Star rating with 10 rating scale

Copyright 2024 - Atom Lab | Privacy Policy | Terms and Conditions