The other day I implemented a component based on a design which was about a unique vertical slider for selecting the user's height. I started to look around for already existing solutions but nothing seemed to fit, so I created one from scratch. Hope it helps someone out there.
TLDR
I created a repo in case you just want to spin up a test project and try things out.
Check it here
Starting point
Firstly, you need to create a React Native project. I won't go into details as there are plenty of nice articles in the topic.
Here is a link about setting up all the things
We will build the slider in the App.js
file.
The building blocks
After starting a React Native project we can finally code.
Let's add the basics of our slider. The code below contains the parts of it with some basic styles. I'll run through these in a bit.
import React from 'react';
import {SafeAreaView, StyleSheet, Text, View} from 'react-native';
const App = () => {
return (
<SafeAreaView>
<Text style={styles.title}>What's your height?</Text>
<View style={styles.slider}>
<View style={styles.rail}>
<View style={styles.railFill} />
</View>
<View style={styles.stepper} />
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
title: {
textAlign: 'center',
fontSize: 20,
marginVertical: 50,
},
slider: {
width: 50,
height: '80%',
marginLeft: 'auto',
marginRight: 'auto',
position: 'relative',
marginBottom: 50,
},
rail: {
width: 20,
height: '100%',
marginLeft: 'auto',
marginRight: 'auto',
backgroundColor: '#DBDBDB',
},
stepper: {
width: '100%',
height: 5,
backgroundColor: 'black',
},
});
export default App;
Slider
This View
contains all of our slider content. I set the height to 80%
instead of using pixels to prevent accidents in smaller phones.
Rail
The rail is the place where the stepper will actually slide up and down. It contains a View
which is going to fill the rail indicating where we are in the slider.
Stepper
Grabbing the stepper we can actually move it to set a value for the slider and also indicating the status.
At this point we have a not too good looking slider which doesn't respond to anything:
Functionality
Let's make this slider respond to the outside world. For this we can use React Native's PanResponder
and Animated
API. These two APIs can work together really nicely, so let's use them.
Firstly we need to calculate our slider's dimensions.
We can use the state for this:
const [sliderDimensions, setSliderDimensions] = useState({
height: null,
top: null,
bottom: null,
});
We'll store the slider's height
, top
and bottom
values here. The top
and bottom
values are the boundaries of the slider on the screen.
To get these values we need to use the onLayout
function on the slider View
. With this we can get the component's dimensions when mount or layout change occurs:
<View
style={styles.slider}
onLayout={(evt) => {
const {height, y} = evt.nativeEvent.layout;
setSliderDimensions({
height: height,
top: y,
bottom: y + height,
});
}}>
<View style={styles.rail}>
<View style={styles.railFill} />
</View>
<View style={styles.stepper} />
</View>
After we have the dimensions we create two Animated
instances. One for the stepper and one for the rail fill.
const stepperAnim = useRef(new Animated.Value(0)).current;
const railFillAnim = useRef(new Animated.Value(0)).current;
<View
style={styles.slider}
onLayout={(evt) => {
const {height, y} = evt.nativeEvent.layout;
setSliderDimensions({
height,
top: y,
bottom: y + height,
});
}}>
<View style={styles.rail}>
<Animated.View style={styles.railFill} />
</View>
<Animated.View style={styles.stepper} />
</View>
Last part for the functionality is the PanResponder
. We can create a responder with the create
method. Here we need to implement four callbacks.
const stepperResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
stepperAnim.setOffset(stepperAnim._value);
railFillAnim.setOffset(railFillAnim._value);
},
onPanResponderMove: (evt, {dy, moveY}) => {
if (moveY > sliderDimensions.top && moveY < sliderDimensions.bottom) {
stepperAnim.setValue(dy);
railFillAnim.setValue(-dy);
}
},
onPanResponderRelease: () => {
stepperAnim.flattenOffset();
railFillAnim.flattenOffset();
},
});
onStartShouldSetPanResponder
With returning true
the PanResponder
will become active when there is a gesture on the component.
onPanResponderGrant
This callback fires when the initial gesture occurs which will be the press on the stepper. Here we need to make sure to set the current position of the stepper and the fill as offset. This is important because we want to start the next gesture from the position we stopped before.
We can achieve this with the setOffset
method of the Animated
API.
onPanResponderMove
Here we need to handle the dragging gesture itself. In the callback we get the event, and the gestureState
objects and the second one provides us useful informations.
The dy
field gives us the accumulated distance since the gesture started which means it starts from zero every time. We need to set the stepper animation value to this. If we have an offset on the stepper animation, this value will be added to that.
The moveY
field gives us the vertical location of the movement on the screen. With this, and the already calculated slider dimensions we can create a condition to make the stepper move only between the slider boundaries.
onPanResponderRelease
When the user releases the stepper we need to reset the offset to zero with the flattenOffset
method.
We need to attach the responder to the stepper view and use the animation value to move the stepper along the Y axis. Here is our stepper after the changes:
<Animated.View
{...stepperResponder.panHandlers}
style={[
styles.stepper,
{
transform: [{translateY: stepperAnim}],
},
]}
/>
Finally we need to create the fill color:
// Rail View
<Animated.View style={[styles.railFill, {height: railFillAnim}]} />;
// Rail style
const styles = StyleSheet.create({
//...
railFill: {
width: '100%',
backgroundColor: '#CBAA71',
position: 'absolute',
bottom: 0,
},
});
Making things prettier
We still need to implement the stripes for our slider somehow.
For this we can fill the rail with extra View
s which have a white color, same as the whole app. This will finally give us the colored stripes.
The code below maps through an array and renders a View
with the height of five pixels, and a spacing of another five pixels by setting the bottom position for each.
<Animated.View style={[styles.railFill, {height: railFillAnim}]}>
{sliderDimensions.height
? Array.apply(
null,
Array(Math.floor(sliderDimensions.height / 10)),
).map((item, index) => (
<View
key={index}
style={[styles.railFillSpace, {bottom: index * 10}]}
/>
))
: null}
</Animated.View>
You can see the final code here.
This is it, the last stripe part went a little bit hacky but for now I'm happy it works.
On the other hand I hope this little article encourages you to start experimenting without using third parties. It is a good way to learn new things in a library or framework.
If you have any feedback, or opinion what you would do differently let me know in the comments below.
Top comments (4)
Hi there,
I tried to make a slider following this guide and it's a good guide, but i'm not sure how to do so the stepper stays inside the slider container. My slider has a height of 287 but the stepper somtimes seem to move the whole height of the screen
hello
i am trying to build audio waves based on slider idea
value -- onValueChange and colors similar to soundCloud but more simple
so i tried to take your code into new project of mine but it didnt responded to my gestures
i need some help with your code please contact me
Regards Maher,
Hi Maher!
Can you paste a repo here so I can have a look?
I'll also update the article and include value calculations later on.
Very useful, thanks for all the answers, solved the problem in my open source project
nextjs full stack: github.com/huanghanzhilian/c-shopping
react native (expo) app: github.com/huanghanzhilian/c-shopp...