Create Tinder Style Swipe Cards with Ionic Gestures

Create Tinder Style Swipe Cards with Ionic Gestures

Follow Josh Morony on

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.

Example of Tinder style swipe cards implemented with Ionic

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:

Example of Tinder style swipe cards implemented with 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:

Example of Tinder style swipe cards implemented with Ionic

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.

Check out my latest videos: