May 8, 2019
It’s perhaps not suprising that gesture support isn’t particularly common on the web. Adding intuitive gestures that match their native counterparts can seem quite daunting and often not worth the trouble. But c’mon, it’s 2019 and the mobile web deserves some decent gesture support!
This article will walk you through the creation of a basic list item which allows you to swipe left to “like” it — a pattern fairly common in native apps, and one that I use quite often in Spotify. Here’s what our final version will look like:
Before getting started, it’s useful to have a basic understanding of React and React hooks. Ready? Let’s do this.
Before adding any gestures, we need a basic component skeleton & layout. Let’s create a component with a wrapper div element and two children: one will be our background with the heart icon, and the other will provide our sliding foreground element.
Our component will look something like this:
import React from 'react'
import { Heart } from 'react-feather'
function Slider({ children }) {
return (
<div className="list-item">
<div className="background">
<Heart />
</div>
<div className="sliding-pane">{children}</div>
</div>
)
}
Our styles are fairly simple. Let’s use a predefined width and height for the list-item
element and give it a relative position. And then let’s assign an absolute position to our sliding-pane
. Our full css will look like this:
.list-item {
position: relative;
width: 300px;
height: 75px;
box-sizing: border-box;
display: flex;
align-items: center;
text-align: center;
border-radius: 0.5rem;
}
.sliding-pane {
cursor: -webkit-grab;
background-color: #121212;
color: rgba(255, 255, 255, 0.9);
position: absolute;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.5rem;
font-size: 1.25em;
font-weight: 600;
}
.sliding-pane:active {
cursor: -webkit-grabbing;
}
.background {
align-self: stretch;
display: flex;
flex: 1;
justify-content: flex-end;
align-items: center;
}
Alright, we have our basic skeleton. But it doesn’t do anything! Time for some gestures.
To add gestures we are going to use a library that I developed called react-gesture-responder. React gesture responder is a hook that binds to an element and provides gesture callbacks for that element. It’s a fairly low-level library that allows you to build (and negotiate between) complex gestures. Let’s add it to our component:
import { useGestureResponder } from 'react-gesture-responder'
function Slider({ children }) {
const { bind } = useGestureResponder({ // the view should claim the responder when touched onStartShouldSet: () => true, onMove: state => { // move callback }, onRelease: state => { // release callback }, })
return (
<div className="list-item">
<div className="background">
<Heart />
</div>
<div {...bind} className="sliding-pane"> {children}
</div>
</div>
)
}
The useGestureResponder
hook returns a bind
object and accepts a number of callbacks. These callbacks help us control and respond to our gestures.
The bind
object should be spread on the element you want to accept gesture controls. What is the bind
object, exactly? It’s a collection of event callbacks, such as onTouchStart
, onTouchMove
, etc., which the hook uses to determine its gestures.
The onStartShouldSet
callback tells the view to claim
the responder for itself. Typically, you’ll always return true for this unless you want to disable gesture interactions altogether. (Note: you may want to return false in more complex cases involving negotation between competing gestures. But that’s for another tutorial.)
The onMove
function is called when a drag is performed, while onRelease
is called when that drag ends.
Let’s use these callbacks to update the position of our slider.
function Slider({ children }) {
const [x, setX] = useState(0)
const { bind } = useGestureResponder({
onStartShouldSet: () => true,
onMove: state => { const [x] = state.delta setX(x) }, onRelease: state => { setX(0) }, })
return (
<div className="list-item">
<div className="background">
<Heart />
</div>
<div
{...bind} style={{ transform: `translateX(${x}px)`, transition: `transform 0.2s ease`,
}}
className="sliding-pane"
>
{children}
</div>
</div>
)
}
Our slider now responds to our drag movements and animates back into place upon release.
But there are a few problems with this code:
My preferred solution to these problems is to use an animation library like react-spring.
React-spring exposes a useSpring
hook which we can use in place of our x
state.
Let’s replace this:
const [x, setX] = useState(0)
with useSpring
:
import { useSpring } from 'react-spring'
const [{ x }, set] = useSpring(() => {
return { x: 0 }
})
We now perform animation updates by calling the set
function instead of setX
.
set({ x: 100 })
React-spring also exports an animated
component. These should be used in place of regular div
elements when using spring values such as our x
value. Let’s put this together:
import { useSpring, animated } from 'react-spring'
function Slider({ children }) {
const [{ x }, set] = useSpring(() => { return { x: 0 } })
const { bind } = useGestureResponder({
onStartShouldSet: () => true,
onMove: state => {
const [x] = state.delta
set({ x, immediate: true }) // the immediate flag bypasses the transition animation },
onRelease: state => {
set({ x: 0, immediate: false }) },
})
return (
<div className="list-item">
<div className="background">
<Heart />
</div>
<animated.div {...bind} style={{ transform: x.interpolate(x => `translateX(${x}px)`), }} className="sliding-pane"
>
{children}
</animated.div>
</div>
)
}
This provides the same functionality as the previous example but fixes both problems mentioned earlier. Performance is improved. And notice the immediate
option when calling our set function? This disables the spring transition animation, which provides us with responsive dragging. We disable immediate
on release to enable the animation.
Lastly, we need to determine when the user has gestured far enough to the left to trigger a “like”, and we need to provide visual feedback to the user when they have reached this threshold.
function Slider({ children }) {
const [isLiking, setIsLiking] = React.useState(false);
const [{ x }, set] = useSpring(() => {
return { x: 0 }
})
function shouldLike(x) { return x < -100 }
const { bind } = useGestureResponder({
onStartShouldSet: () => true,
onMove: state => {
const [x] = state.delta
const like = shouldLike(x) if (like !== isLiking) { setIsLiking(like) }
set({ x, immediate: true })
},
onRelease: state => {
if (shouldLike(state.delta[0])) { console.log('User has liked!) }
set({ x: 0, immediate: false })
},
})
return (
<div className="list-item">
<div className="background">
<Heart
style={{
color: 'white', fill: isLiking ? 'white' : 'transparent', transition: 'transform 0.3s ease' }}
/>
</div>
<animated.div
{...bind}
style={{
transform: transform: x.interpolate(x => `translateX(${x}px)`),
}}
className="sliding-pane"
>
{children}
</animated.div>
</div>
)
}
Our move callback checks to see if the user has gestured far enough to the like, and updates our isLiked
state accordingly. Our heart icon changes appearance in response. But we don’t actually trigger the like event until the gesture has ended in order to give the user the chance to cancel their gesture.
This gesture can be improved in a few ways:
You can see both of these in the final version:
I’ve created a few open source components that respond to gestures which might be useful for learning.
Got others you want to share? I’d love to see them!