React Native — Swipe to Start

How to build the effect in ⏲️ 30 min

Mikael Ainalem
Stackademic

--

iPhone’s slide to unlock interaction

The iconic ‘swipe to start’ gesture is both playful and remarkably simple to implement. It served as a cornerstone feature of the original iPhone, catapulting the device to stardom. Signature moments like ‘slide to unlock,’ ‘slide to answer,’ and ‘slide to power off’ where all moments of delight that really stood out. What makes this interaction so interesting is its intuitive nature; users instinctively understand how to engage with this widget the moment it appears on the screen.

This tutorial goes through the steps to implement the interaction using nothing but a bare-bone React Native. The design used in this tutorial is mostly based on this brilliant dribbble shot by Anik Deb. Let’s go!

Step 1️⃣ — containers, colors, and border radius

First version

The first step is to is to get the fundamentals of the UI in place. Let’s create a separate component consisting of 3 sub-components: A container, a text “Swipe to start” and the slider. The slider, with its oval shape, is essentially a rounded rectangle. This is what the code looks like:

const SwipeToStart = () => (
<View style={styles.container}>
<Text style={styles.text}>Swipe to start</Text>
<View style={styles.slider} />
</View>
);

And the styling:

const styles = StyleSheet.create({
container: {
backgroundColor: '#2D6844',
borderRadius: 44,
height: 88,
justifyContent: 'center',
marginTop: 100,
position: 'relative',
},
text: {
color: 'white',
fontSize: 22,
fontWeight: '600',
marginLeft: 40,
},
slider: {
backgroundColor: 'white',
borderRadius: 32,
height: 64,
position: 'absolute',
width: 92,
top: 12,
right: 12,
},
});

Step 2️⃣ — Chevrons, chevrons, and chevrons

Adding the chevrons

To craft the arrows, we use two views for each Chevron, slightly tilted using a transform. Positioning one view on top and the other below forms a V-shaped object resembling a chevron standing on its side. Once the component is created, we can easily replicate it three times to achieve the visual effect of three arrows in a row.

const Chevron = ({style}: {style: ViewStyle}) => (
<View style={style}>
<View style={styles.upperWing />
<View style={styles.lowerWing />
</View>
);

And the style:

const styles = StyleSheet.create({
// ...
upperWing {
backgroundColor: '#000',
height: 14,
width: 3,
borderRadius: 1,
position: 'absolute',
top: 20,
transform: [{rotate: '35deg'}],
},
lowerWing {
backgroundColor: '#000',
height: 14,
width: 3,
borderRadius: 1,
position: 'absolute',
top: 30,
transform: [{rotate: '-35deg'}],
},
chevron1: {left: 25},
chevron2: {left: 61},
chevron3: {left: 43},
});

Step 3️⃣ — Shimmering lights

Glowing chevrons

A nice touch to the UI, and something that enhances the UX, is a cascading shimmering animation. This effect provides visual feedback to the user, indicating how to interact with the slider and in which direction.

The first part of this step is to create an Animated.Value that enables us to interpolate colors. Next, we initiate and run the animation indefinitely. This is achieved by a useEffect callback that runs when the component is mounted. Additionally, we supply a separate lambda function to the Animated.timing.start callback to establish a chain of animations. This series of animations continuously animates the chevrons at 2-second intervals. Below is the code for the entire process:

const SwipeToStart = ({}: Props) => {
const chevronColorAnim = useRef(new Animated.Value(0)).current;

const shimmer = () => {
Animated.timing(chevronColorAnim, {
toValue: 1,
duration: 2000,
useNativeDriver: false,
}).start(() => {
chevronColorAnim.setValue(0);
// restart the animation after a cycle is completed
shimmer();
});
};

useEffect(() => {
// Start the animation when the component is mounted
shimmer();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// ...
}

Next, let’s have a look at the Chevron component. Once the Views get their color from the previously created Animated.Value, the component takes on its final appearance. It’s important to note that we need to change the Chevron Views into Animated.Views to enable them to dynamically change their backgroundColor.

const Chevron = ({
style,
animated,
inputRange,
outputRange,
}: {
style: ViewStyle;
animated: Animated.Value;
inputRange: number[];
outputRange: string[];
}) => {
const chevronColor = animated.interpolate({
inputRange,
outputRange,
});

return (
<View style={style}>
<Animated.View
style={[styles.upperChevron, {backgroundColor: chevronColor}]}
/>
<Animated.View
style={[styles.lowerChevron, {backgroundColor: chevronColor}]}
/>
</View>
);
};

Last, but not least, is the cascading. I.e. offsetting the timing of each animation. Here is how we create the effect in the SwipeToStart component’s JSX code:

  // ...
<View style={styles.slider}>
<Chevron
style={styles.chevron1}
animated={chevronColorAnim}
inputRange={[0, 0.2, 0.7, 1]}
outputRange={['#2C472A', '#000000', '#6fb268', '#2C472A']}
/>
<Chevron
style={styles.chevron2}
animated={chevronColorAnim}
inputRange={[0, 0.1, 0.6, 1.0]}
outputRange={['#162415', '#000000', '#6fb268', '#162415']}
/>
<Chevron
style={styles.chevron3}
animated={chevronColorAnim}
inputRange={[0, 0.5, 1]}
outputRange={['#000000', '#6fb268', '#000000']}
/>
</View>
// ...

Pay attention to how the inputRange is shifted to make the color seemingly move leftwards.

Step 4️⃣ — Enable dragging

Dragging the slider

Next is enabling dragging of the slider. To get code we simply ask ChatGPT. With slight modifications the dragging is quickly enabled.

const SwipeToStart = ({}: Props) => {
// ...

const translationX = useRef(new Animated.Value(0)).current;

const release = () => {
Animated.spring(translationX, {
toValue: 0,
useNativeDriver: false,
}).start();
};

const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (_, gestureState) => {
translationX.setValue(gestureState.dx);
},
onPanResponderRelease: () => {
release();
},
}),
).current;

return (
<View style={styles.container}>
<Text style={styles.text}>Swipe to start</Text>
<Animated.View
style={[styles.slider, {transform: [{translateX: translationX}]}]}
{...panResponder.panHandlers}>
<Chevron /* ... */ />
<Chevron /* ... */ />
<Chevron /* ... */ />
</Animated.View>
</View>
);

Step 5️⃣ — Staying inside the perimeter

Forcing the slider to stay inside the box

To force the slider to stay inside the we measure the component. If you’re a bit lazy, you can hard-code distance the slider can move. Measuring the outer container makes the component slightly more flexible. Once we have the value, we can use it to determine how to clamp the movement of the slider.

const SwipeToStart = ({}: Props) => {
const distance = useRef(0);

const onLayout = (event: LayoutChangeEvent) => {
const {width} = event.nativeEvent.layout;
distance.current = width - SLIDER_WIDTH - SLIDER_MARGIN * 2;
};

// ...

const panResponder = useRef(
PanResponder.create({
// ...
onPanResponderMove: (_, gestureState) => {
console.log('gestureState.dx', gestureState.dx);

// Force the slider to stay inside the container
if (gestureState.dx > 0) {
translationX.setValue(0);
} else if (gestureState.dx < -distance.current) {
translationX.setValue(-distance.current);
} else {
translationX.setValue(gestureState.dx);
}
},
// ...
}),
).current;

// ...

return (
<View style={styles.container} onLayout={onLayout}>
// ...
);
};

Step 6️⃣️ — Release

The whole thing

Last but not least is the action itself.

type Props = {
onStart: () => void;
};

const SwipeToStart = ({onStart}: Props) => {
// ...
const panResponder = useRef(
PanResponder.create({
// ...
onPanResponderRelease: (_, gestureState) => {
if (gestureState.dx < -distance.current) {
onStart();
}

release();
},
}),
).current;

// ...
};

That’s it! Thanks for reading thus far! As always, please support my work. Clap, share, follow, …

Cheers!

You can find the source code here: https://github.com/ainalem/SwipeToGetStarted

Stackademic 🎓

Thank you for reading until the end. Before you go:

--

--

Enthusiastic about software & design, father of 3, freelancer and currently CTO at Norban | twitter: https://twitter.com/mikaelainalem