I’ve been with my wife since around the time Tinder was created, so I’ve never had the experience of swiping left or right myself. For whatever reason, swiping caught on in a big way. The Tinder animated swipe card UI seems to have become extremely popular and something people want to implement in their own applications. Without looking too much into why this provides an effective user experience, it does seem to be a great format for prominently displaying relevant information and then having the user commit to making an instantaneous decision on what has been presented.
Creating this style of animation/gesture has always been possible in Ionic applications - you could use one of many libraries to help you, or you could have also implemented it from scratch yourself. However, now that Ionic is exposing their underlying gesture system for use by Ionic developers, it makes things significantly simpler. We have everything we need out of the box, without having to write complicated gesture tracking ourselves.
I recently released an overview of the new Gesture Controller in Ionic 5 which you can check out below:
If you are not already familiar with the way Ionic handles gestures within their components, I would recommend giving that video a watch before you complete this tutorial as it will give you a basic overview. In the video, we implement a kind of Tinder “style” gesture, but it is at a very basic level. This tutorial will aim to flesh that out a bit more, and create a more fully implemented Tinder swipe card component.
We will be using StencilJS to create this component, which means that it will be able to be exported and used as a web component with whatever framework you prefer (or if you are using StencilJS to build your Ionic application you could just build this component directly into your Ionic/StencilJS application). Although this tutorial will be written for StencilJS specifically, it should be reasonably straightforward to adapt it to other frameworks if you would prefer to build this directly in Angular, React, etc. Most of the underlying concepts will be the same, and I will try to explain the StencilJS specific stuff as we go.
NOTE: This tutorial was built using Ionic 5 which, at the time of writing this, is currently in beta. If you are reading this before Ionic 5 has been fully released, you will need to make sure to install the @next
version of @ionic/core
or whatever framework specific Ionic package you are using, e.g. npm install @ionic/core@next
or npm install @ionic/angular@next
.
Before We Get Started
If you are following along with StencilJS, I will assume that you already have a basic understanding of how to use StencilJS. If you are following along with a framework like Angular, React, or Vue then you will need to adapt parts of this tutorial as we go.
If you would like a thorough introduction to building Ionic applications with StencilJS, you might be interested in checking out my book.
A Brief Introduction to Ionic Gestures
As I mentioned above, it would be a good idea to watch the introduction video I did about Ionic Gesture, but I will give you a quick rundown here as well. If we are using @ionic/core
we can make the following imports:
import { Gesture, GestureConfig, createGesture } from '@ionic/core';
This provides us with the types for the Gesture
we create, and the GestureConfig
configuration options we will use to define the gesture, but most important is the createGesture
method which we can call to create our “gesture”. In StencilJS we use this directly, but if you are using Angular for example, you would instead use the GestureController
from the @ionic/angular
package which is basically just a light wrapper around the createGesture
method.
In short, the “gesture” we create with this method is basically mouse/touch movements and how we want to respond to them. In our case, we want the user to perform a swiping gesture. As the user swipes, we want the card to follow their swipe, and if they swipe far enough we want the card to fly off screen. To capture that behaviour and respond to it appropriately, we would define a gesture like this:
const options: GestureConfig = {
el: this.hostElement,
gestureName: 'tinder-swipe',
onStart: () => {
// do something as the gesture begins
},
onMove: (ev) => {
// do something in response to movement
},
onEnd: (ev) => {
// do something when the gesture ends
},
};
const gesture: Gesture = await createGesture(options);
gesture.enable();
This is a bare-bones example of creating a gesture (there are additional configuration options that can be supplied). We pass the element we want to attach the gesture to through the el
property - this should be a reference to the native DOM node (e.g. something you would usually grab with a querySelector
or with @ViewChild
in Angular). In our case, we would pass in a reference to the card element that we want to attach this gesture to.
Then we have our three methods onStart
, onMove
, and onEnd
. The onStart
method will be triggered as soon as the user starts a gesture, the onMove
method will trigger every time there is a change (e.g. the user is dragging around on the screen), and the onEnd
method will trigger once the user releases the gesture (e.g. they let go of the mouse, or lift their finger off the screen). The data that is supplied to us through ev
can be used to determine a lot, like how far the user has moved from the origin point of the gesture, how fast they are moving, in what direction, and much more.
This allows us to capture the behaviour we want, and then we can run whatever logic we want in response to that. Once we have created the gesture, we just need to call gesture.enable
which will enable the gesture and start listening for interactions on the element it is associated with.
With this idea in mind, we are going to implement the following gesture/animation in Ionic:
1. Create the Component
You will need to create a new component, which you can do inside of a StencilJS application by running:
npm run generate
You may name the component however you wish, but I have called mine app-tinder-card
. The main thing to keep in mind is that component names must be hyphenated and generally you should prefix it with some unique identifier as Ionic does with all of their components, e.g. <ion-some-component>
.
2. Create the Card
We can apply the gesture we will create to any element, it doesn’t need to be a card or sorts. However, we are trying to replicate the Tinder style swipe card, so we will need to create some kind of card element. You could, if you wanted to, use the existing <ion-card>
element that Ionic provides. To make it so that this component is not dependent on Ionic, I will just create a basic card implementation that we will use.
Modify
src/components/tinder-card/tinder-card.tsx
to reflect the following:
import {
Component,
Host,
Element,
Event,
EventEmitter,
h,
} from '@stencil/core';
import { Gesture, GestureConfig, createGesture } from '@ionic/core';
@Component({
tag: 'app-tinder-card',
styleUrl: 'tinder-card.css',
})
export class TinderCard {
render() {
return (
<Host>
<div class="header">
<img class="avatar" src="https://avatars.io/twitter/joshuamorony" />
</div>
<div class="detail">
<h2>Josh Morony</h2>
<p>Animator of the DOM</p>
</div>
</Host>
);
}
}
We have added a basic template for the card to our render()
method. For this tutorial, we will just be using non-customisable cards with the static content you see above. You may want to extend the functionality of this component to use slots or props so that you can inject dynamic/custom content into the card (e.g. have other names and images besides “Josh Morony”).
It is also worth noting that we have set up all of the imports we will be making use of:
import {
Component,
Host,
Element,
Event,
EventEmitter,
h,
} from '@stencil/core';
import { Gesture, GestureConfig, createGesture } from '@ionic/core';
We have our gesture imports, but as well as that we are importing Element
to allow us to get a reference to the host element (which we want to attach our gesture to). We are also importing Event
and EventEmitter
so that we can emit an event that can be listened for when the user swipes right or left. This would allow us to use our component in this manner:
<app-tinder-card
onMatch={(ev) => {
this.handleMatch(ev);
}}
/>
So that our cards don’t look completely ugly, we are going to add a few styles as well.
Modify
src/components/tinder-card/tinder-card.css
to reflect the following:
app-tinder-card {
display: block;
width: 100%;
min-height: 400px;
border-radius: 10px;
display: flex;
flex-direction: column;
box-shadow: 0 0 3px 0px #cecece;
}
.header {
background-color: #36b3e7;
border: 4px solid #fbfbfb;
border-radius: 10px 10px 0 0;
display: flex;
justify-content: center;
align-items: center;
flex: 2;
}
.avatar {
width: 200px;
height: auto;
}
.detail {
background-color: #fbfbfb;
padding-left: 20px;
border-radius: 0 0 10px 10px;
flex: 1;
}
3. Define the Gesture
Now we are getting into the core of what we are building. We will define our gesture and the behaviour that we want to trigger when that gesture happens. We will first add the code as a whole, and then we will focus on the interesting parts in detail.
Modify
src/components/tinder-card/tinder-card.tsx
to reflect the following:
import {
Component,
Host,
Element,
Event,
EventEmitter,
h,
} from '@stencil/core';
import { Gesture, GestureConfig, createGesture } from '@ionic/core';
@Component({
tag: 'app-tinder-card',
styleUrl: 'tinder-card.css',
})
export class TinderCard {
@Element() hostElement: HTMLElement;
@Event() match: EventEmitter;
connectedCallback() {
this.initGesture();
}
async initGesture() {
const style = this.hostElement.style;
const windowWidth = window.innerWidth;
const options: GestureConfig = {
el: this.hostElement,
gestureName: 'tinder-swipe',
onStart: () => {
style.transition = 'none';
},
onMove: (ev) => {
style.transform = `translateX(${ev.deltaX}px) rotate(${
ev.deltaX / 20
}deg)`;
},
onEnd: (ev) => {
style.transition = '0.3s ease-out';
if (ev.deltaX > windowWidth / 2) {
style.transform = `translateX(${windowWidth * 1.5}px)`;
this.match.emit(true);
} else if (ev.deltaX < -windowWidth / 2) {
style.transform = `translateX(-${windowWidth * 1.5}px)`;
this.match.emit(false);
} else {
style.transform = '';
}
},
};
const gesture: Gesture = await createGesture(options);
gesture.enable();
}
render() {
return (
<Host>
<div class="header">
<img class="avatar" src="https://avatars.io/twitter/joshuamorony" />
</div>
<div class="detail">
<h2>Josh Morony</h2>
<p>Animator of the DOM</p>
</div>
</Host>
);
}
}
At the beginning of this class, we have set up the following code:
@Element() hostElement: HTMLElement;
@Event() match: EventEmitter;
connectedCallback(){
this.initGesture();
}
The @Element()
decorator will provide us with a reference to the host element of this component. We also set up a match
event emitter using the @Event()
decorator which will allow us to listen for the onMatch
event to determine which direction a user swiped.
We have set up the connectedCallback
lifecycle hook to automatically trigger our initGesture
method which is what handles actually setting up the gesture. We have already discussed the basics of defining a gesture, so let’s focus on our specific implementation of the onStart
, onMove
, and onEnd
methods:
onStart: () => {
style.transition = "none";
},
onMove: (ev) => {
style.transform = `translateX(${ev.deltaX}px) rotate(${ev.deltaX/20}deg)`
},
onEnd: (ev) => {
style.transition = "0.3s ease-out";
if(ev.deltaX > windowWidth/2){
style.transform = `translateX(${windowWidth * 1.5}px)`;
this.match.emit(true);
} else if (ev.deltaX < -windowWidth/2){
style.transform = `translateX(-${windowWidth * 1.5}px)`;
this.match.emit(false);
} else {
style.transform = ''
}
}
Let’s being with the onMove
method. When the user swipes on the card, we want the card to follow the movement of that swipe. We could just detect the swipe and animate the card after the swipe has been detected, but this isn’t as interactive and won’t look as nice/smooth/intuitive. So, what we do is modify the transform
property of the elements style to modify the translateX
to match the deltaX
of the movement. The deltaX
is the distance the gesture has moved from the initial start point in the horizontal direction. The translateX
will move an element in a horizontal direction by the number of pixels we supply. If we set this translateX
to the deltaX
it will mean that the element will follow our finger, or mouse, or whatever we are using for input along the screen.
We also set the rotate
transform so that the card rotates in relation to a ratio of the horizontal movement - the further you get to the edge of the screen, the more the card will rotate. This is divided by 20
just to lessen the effect of the rotation - try setting this to a smaller number like 5
or even just use ev.deltaX
directly and you will see how ridiculous it looks.
The above gives us our basic swiping gesture, but we don’t want the card to just follow our input - we need it to do something after we let go. If the card isn’t near enough the edge of the screen it should snap back to its original position. If the card has been swiped far enough in one direction, it should fly off the screen in the direction it was swiped.
First, we set the transition
property to 0.3s ease-out
so that when we reset the cards position back to translateX(0)
(if the card was no swiped far enough) it doesn’t just instantly pop back into place - instead, it will animate back smoothly. We also want the cards to animate off screen nicely, we don’t want them to just pop out of existence when the user lets go.
To determine what is “far enough”, we just check if the deltaX
is greater than half the window width, or less than half of the negative window width. If either of those conditions are satisfied, we set the appropriate translateX
such that the card goes off the screen. We also trigger the emit
method on our EventListener
so that we can detect the successful swipe when using our component. If the swipe was not “far enough” then we just reset the transform
property.
One more important thing we do is set style.transition = "none";
in the onStart
method. The reason for this is that we only want the translateX
property to transition between values when the gesture has ended. There is no need to transition between values onMove
because these values are already very close together, and attempting to animate/transition between them with a static amount of time like 0.3s
will create weird effects.
4. Use the Component
Our component is complete! Now we just need to use it, which is reasonably straight-forward with one caveat which I will get to in a moment. Using the component directly in your StencilJS application would look something like this:
import { Component, h } from '@stencil/core';
@Component({
tag: 'app-home',
styleUrl: 'app-home.css',
})
export class AppHome {
handleMatch(ev) {
if (ev.detail) {
console.log("It's a match!");
} else {
console.log('Maybe next time');
}
}
render() {
return [
<ion-header>
<ion-toolbar color="primary">
<ion-title>Ionic Tinder Cards</ion-title>
</ion-toolbar>
</ion-header>,
<ion-content class="ion-padding">
<div class="tinder-container">
<app-tinder-card
onMatch={(ev) => {
this.handleMatch(ev);
}}
/>
<app-tinder-card
onMatch={(ev) => {
this.handleMatch(ev);
}}
/>
<app-tinder-card
onMatch={(ev) => {
this.handleMatch(ev);
}}
/>
</div>
</ion-content>,
];
}
}
We can mostly just drop our app-tinder-card
right in there, and then just hook up the onMatch
event to some handler function as we have done with the handleMatch
method above.
One thing we have not covered in this tutorial is handling a “stack” of cards, as these Tinder cards would usually be used in. What would likely be the nicer option is to create an additional <app-tinder-card-stack>
component, such that it could be used like this:
<app-tinder-card-stack>
<app-tinder-card />
<app-tinder-card />
<app-tinder-card />
</app-tinder-card-stack>
and the styling for positioning the cards on top of one another would be handled automatically. However, for now, I have just added some manual styling directly in the page to position the cards directly:
.tinder-container {
position: relative;
}
app-tinder-card {
position: absolute;
top: 0;
left: 0;
}
Which will give us something like this:
Summary
It’s pretty fantastic to be able to build what is a reasonably cool/complex looking animated gesture, all with what we are given out of the box with Ionic. The opportunities here are effectively endless, you could create any number of cool gestures/animations using the basic concept of listening for the start, movement, and end events of gestures. This is also using just the bare-bones features of Ionic’s gesture system as well, there are more advanced concepts you could make use of (like conditions in which a gesture is allowed to start).
I wanted to focus mainly on the gestures and animation aspect of this functionality, but if there is interest in covering the concept of a <app-tinder-card-stack>
component to work in conjunction with the <app-tinder-card>
component let me know in the comments.