DEV Community

Cover image for Creating a Tinder-like Swipe UI on Vue
yev
yev

Posted on • Updated on

Creating a Tinder-like Swipe UI on Vue

Have you ever wondered how that swipe-right-swipe-left, tinder-like user experience was built? I did, several days ago. I come from more of a backend background, and to my uninitiated mind, I find this sort of thing really amazing.

I was curious, how hard would it be for an average mediocre developer like me to build something cool like that?

Reconnaisance

Gathering information was always my first step when working on new projects. I don't start experimenting with any code, I google first. I mean, surely someone smarter than me has already thought of this before.

And surely enough, after searching for "vue swipeable cards", first thing google gave me was just that (lucky me).

It's an article from css-tricks by Mateusz Rybczonek about building swipeable cards using interact.js.

The article will explain how a swipeable component itself is built far better than me. What's more important is that he extracted the functionality and published it to npm as vue2-interact (yay open source!).

While the article did explain how everything works, it's essentially just boilerplate code for us. What we need is to actually use the extracted functionality itself. That's why the Vue2InteractDraggable was a blessing, all the heavy-lifting was already done for us, it's just a matter of figuring out how we would use it on our own project.

Experiment

At this point, all I need to do is play with it. The docs are pretty clear. Let's start of with the most simplest code that we can interact with:

<template>
  <section class="container">
    <div class="fixed-center">
      <Vue2InteractDraggable
        :interact-out-of-sight-x-coordinate="500"
        :interact-max-rotation="15"
        :interact-x-threshold="200"
        :interact-y-threshold="200"
        class="rounded-borders shadow-10 card">
        <div class="card__main">    
        </div>
      </Vue2InteractDraggable>
    </div>
  </section>
</template>

<script>
import { Vue2InteractDraggable } from 'vue2-interact'

export default {
  name: 'SwipeableCards',
  components: { Vue2InteractDraggable }
}
</script>

Nothing fancy, this just displays a pink box at the center screen that I can drag around.

pink-box

Cool, cool, cool, cool. It's working alright. Now that we've verified that, It's time to think about the rest of the stuff that I want to accomplish.

To demonstrate the kind of user interaction that I was hoping, I've boiled things down to the following requirements.

  1. Detect if the card was dragged out of view and hide it.
  2. Stack the draggable cards on top of each other.
  3. Be able to control the swiping action of the swipe gesture (programmatically trigger via buttons).

Problem #1: Detect and Hide

Problem #1 is quite simple, Vue2InteractDraggable component emits drag* events when interact-out-of-sight-*-coordinate was exceeded, it also hides the component automatically.
hide-on-drag

Problem #2: Stack the cards

Problem #2 is quite tricky. The Vue2InteractDraggable is technically just a single draggable component. UI-wise, stacking them could be as simple as using css to implement a combination of z-index, width, and box-shadow to emulate depth. But would the swipe component still work? Well, I could stop pointer-eventson the bottommost cards to avoid any side-effects.

Let's try it out. I'll use an array, popping off the first element on each swipe-right. Seems reasonable, right?

Here's the code so far:

<template>
  <section class="container">
    <div>
        <Vue2InteractDraggable
          v-for="(card, index) in cards"
          :key="index"
          :interact-out-of-sight-x-coordinate="500"
          :interact-max-rotation="15"
          :interact-x-threshold="200"
          :interact-y-threshold="200"
          @draggedRight="right"
          class="rounded-borders card fixed fixed--center"
          :class="{
            'card--top': index === 0
          }">
          <div class="flex flex--center" style="height: 100%">
            <h1>{{card.text}}</h1>
          </div>
      </Vue2InteractDraggable>
    </div>
  </section>
</template>
<script>
import { Vue2InteractDraggable } from 'vue2-interact'

export default {
  name: 'SwipeableCards',
  components: { Vue2InteractDraggable },
  data() {
    return {
      cards: [
        { text: 'one' },
        { text: 'two' },
        { text: 'three' },
      ]
    }
  },
  methods: {
    right() {
      setTimeout(() => this.cards.shift(), 300);
    }
  }
}
</script>

<style lang="scss" scoped>
.container {
  background: #eceff1;
  width: 100%;
  height: 100vh;
}

.flex {
  display: flex;
  &--center {
    align-items: center;
    justify-content: center;
  }
}

.fixed {
  position: fixed;
  &--center {
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
  }
}
.rounded-borders {
  border-radius: 2rem
}
.card {
  pointer-events: none;
  width: 300px;
  height: 400px;
  &:nth-child(1) {
    background: pink;
    z-index: 3;
  }
  &:nth-child(2) {
    z-index: 2;
    background: red;
    top: 52%;
  }
  &:nth-child(3) {
    z-index: 1;
    background: green;
    top: 54%;
  }
  &--top {
    pointer-events: auto !important;
  }
}
</style>

Now this is what I have:
Epic fail
Well, that's a total failure. For some reason, when the event fires for the first card, it also fires for the second card. You can see below that after my first swipe, there are only 2 cards left on the DOM, but we can't see the second card because it's rotated out of view. On the dev tool, we can see that the transform animation style is being set on the second card after swiping the first card (You can see that it pops back when I disabled the style via devtool).

dragfail2

The issue is still there even when I tried to simply lay the cards in rows. I'm not sure why this happens. I must be missing something or it's an issue from the Vue2InteractDraggable component itself.

At this point, I have two options: I can continue on debugging, dig around the actual implementation, possibly backtrack how the original author extracted the functionality to find out what's different, check the github repo for similar issues and try to find answers from there; Or think of a different approach to accomplish the same thing and just circle back on it some other time.

I'm choosing the latter. A different approach might end up just as good as the first one. There's no point in biting off more than I could chew at this time. I could also just visit it again some other time.

Let's move on.

The previous result got me thinking.. If things break everytime I use multiple Vue2InteractDraggable components, why not avoid doing that altogether and just use one? After all, I'm only dragging one card at a time. Why not just use the same card and replace the content accordingly? Coupled with other css mumbo-jumbos, I think this might work.

Let's come up with the simplest code that can verify my assumptions:

<template>
  <section class="container">
    <div class="fixed fixed--center" style="z-index: 3">
      <Vue2InteractDraggable
        v-if="isVisible"
        :interact-out-of-sight-x-coordinate="500"
        :interact-max-rotation="15"
        :interact-x-threshold="200"
        :interact-y-threshold="200"
        @draggedRight="right"
        class="rounded-borders card card--one">
        <div class="flex flex--center" style="height: 100%">
          <h1>{{current.text}}</h1>
        </div>
      </Vue2InteractDraggable>
    </div>
    <div
      class="rounded-borders card card--two fixed fixed--center"
      style="z-index: 2">
      <div class="flex flex--center" style="height: 100%">
        <h1>test</h1>
      </div>
    </div>
    <div
      class="rounded-borders card card--three fixed fixed--center"
      style="z-index: 1">
      <div class="flex flex--center" style="height: 100%">
        <h1>test</h1>
      </div>
    </div>
  </section>
</template>
<script>
import { Vue2InteractDraggable } from 'vue2-interact'

export default {
  name: 'SwipeableCards',
  components: { Vue2InteractDraggable },
  data() {
    return {
      isVisible: true,
      index: 0,
      cards: [
        { text: 'one' },
        { text: 'two' },
        { text: 'three' },
      ]
    }
  },
  computed: {
    current() {
      return this.cards[this.index]
    }
  },
  methods: {
    right() {
      setTimeout(() => this.isVisible = false, 200)
      setTimeout(() => {
        this.index++
        this.isVisible = true
      }, 300)
    }
  }
}
</script>

<style lang="scss" scoped>
.container {
  background: #eceff1;
  width: 100%;
  height: 100vh;
}

.flex {
  display: flex;
  &--center {
    align-items: center;
    justify-items: center;
    justify-content: center;
  }
}

.fixed {
  position: fixed;
  &--center {
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
  }
}
.rounded-borders {
  border-radius: 12px;
}
.card {
  width: 300px;
  height: 400px;
  color: white;
  &--one {
    background-color: pink;
  }
  &--two {
    background-color: red;
    width: 280px;
    top: 51%;
  }
  &--three {
    background-color: orange;
    width: 260px;
    top: 51.8%;
  }
}
</style>

Aand it worked!
it works

This also seems simpler compared to my first approach. I'm reusing the same Vue2InteractDraggable instance instead of instantiating one for each element in the array. We don't even need to stack all the cards anyway, we just need to keep that illusion up.

That being said, I should probably display the next element's content on the card behind the first to further improve the illusion, like so:

<template>
  <section class="container">
    <div class="fixed fixed--center" style="z-index: 3">
      <Vue2InteractDraggable
        v-if="isVisible"
        :interact-out-of-sight-x-coordinate="500"
        :interact-max-rotation="15"
        :interact-x-threshold="200"
        :interact-y-threshold="200"
        @draggedRight="right"
        class="rounded-borders card card--one">
        <div class="flex flex--center" style="height: 100%">
          <h1>{{current.text}}</h1>
        </div>
      </Vue2InteractDraggable>
    </div>
    <div
      v-if="next"
      class="rounded-borders card card--two fixed fixed--center"
      style="z-index: 2">
      <div class="flex flex--center" style="height: 100%">
        <h1>{{next.text}}</h1>
      </div>
    </div>
    <div
      v-if="index + 2 < cards.length"
      class="rounded-borders card card--three fixed fixed--center"
      style="z-index: 1">
      <div class="flex flex--center" style="height: 100%">
        <h1>test</h1>
      </div>
    </div>
  </section>
</template>
<script>
import { Vue2InteractDraggable } from 'vue2-interact'

export default {
  name: 'SwipeableCards',
  components: { Vue2InteractDraggable },
  data() {
    return {
      isVisible: true,
      index: 0,
      cards: [
        { text: 'one' },
        { text: 'two' },
        { text: 'three' },
      ]
    }
  },
  computed: {
    current() {
      return this.cards[this.index]
    },
    next() {
      return this.cards[this.index + 1]
    }
  },
  methods: {
    right() {
      setTimeout(() => this.isVisible = false, 200)
      setTimeout(() => {
        this.index++
        this.isVisible = true
      }, 300)
    }
  }
}
</script>

I'm also hiding the bottommost dummy cards accordingly as I change the topmost card, good 'ol switcheroo.

switcheroo

That worked pretty well. Hiding the dummy cards as we move the index up worked like a charm too. This would probably look even better when we start using images instead of text and colored divs. We could even further boost the illusion by putting some subtle transition animation as the bottommost card become the topmost. But I'll worry about those later, let's move on to the final piece of the puzzle.

Problem #3: Trigger Swipe action via Button Click

Fortunately, this is fairly trivial too. vue2-interact exposes an EventBus that we could use to trigger the drag/swipe actions. According to the docs, it's as simple as supplying the interact-event-bus-events prop with an object that contains the events you need and then using InteractEventBus to trigger the desired action.

<template>
 <Vue2InteractDraggable
  @draggedLeft="draggedLeft"
  :interact-event-bus-events="interactEventBusEvents"
  v-if="isShowing"
  class="card">
  <div>
    <h3 class="cardTitle">Drag me!</h3>
  </div>
 </Vue2InteractDraggable>

 <BaseButton @click="dragLeft" label="⇦" />
</template>
<script>
import { Vue2InteractDraggable, InteractEventBus } from 'vue2-interact'
const INTERACT_DRAGGED_LEFT = 'INTERACT_DRAGGED_LEFT';

export default {
  components: { Vue2InteractDraggable },
  data() {
    return {
      isShowing: true,
      interactEventBusEvents: {
        draggedLeft: INTERACT_DRAGGED_LEFT,
      },
    };
  },

  methods: {
    dragLeft() {
      InteractEventBus.$emit(INTERACT_DRAGGED_LEFT);
    },
  }
};
</script>

Essentially, we just told the component to trigger the draggedLeft event everytime we $emit an INTERACT_DRAGGED_LEFT over at the InteractEventBus.

And with that, I think we got all the things we need to start putting it all together.

Putting it all together

I downloaded some images from unsplash and scaled it down for my purposes. I used those images as the value of my array so I could replace the texts and removed the background colors. I also realized that it's simpler to enhance the illusion if I change the orientation of my card stack. Instead of stacking it upwards, I stacked them diagonally. In this way, my transition animation could be as simple as taking the x and y translation of the second card and putting it on the first as the switch happens. I won't bore you by showing all the steps I took, I think you already get the idea, I'll leave it to your imagination.

After dropping in some more css magic, gradients, shadows and stuff. A google font and some material icons. I ended up with something like this:
Kittynder
Behold, Kittynder! Tinder for cats. Does it make sense? I don't know. But it's a pun opportunity. If this is a real app, my cat would probably scratch right on Katrina, they're around the same age, I think they would hit it off.

You can check out the entire code on this github repository: kittynder. I published a demo over at netlify: kittynder.netlify.com. I highly suggest viewing it on a mobile viewport.

Epilogue

In this simple activity, I realized how easy it actually is to build this swipeable, tinder-like UI thing today. This took me just less than two hours to complete. Now more than ever, the amount of tools and resources over the internet is enough for you to build a lot of things, things that seem like something so far out of your league before. This is the power of ̶u̶l̶t̶r̶a̶ ̶i̶n̶s̶t̶i̶n̶c̶t̶ open-source community. That's also one of the reason why I started writing tutorials like this. It's my way of giving back to the community. I might be just a lowly mediocre developer, but still, my thought-process and problem-solving approach could be valuable to people who are just starting out (and to future me, because I will completely forget everything after a year).
honest-work

Next Steps?

Of course, this is by no means production-ready. My css-game is pretty bad, you should probably consider using something like tailwind.css, and pre-cache the images, check browser compatibility, etc.. But hey, it's a good exercise. Step-by-step, you'll eventually get there. Just search, read, and build.

I'm actually implementing something similar on a slightly larger personal project using Quasar Framework but that's a story for a different time.

Useful Links

This article is originally posted on my personal website.

Discussion (12)

Collapse
vipulphysiofirst profile image
Vipul Chaursiya

Hello @yev thanks for this article , I'm using this thought in my site it is doing good, but I stuck in one problem,
that is like I'm using this tinder-like swipe card in testimonial block and it is on the homepage .
when I scroll on the y-axis it is not scrolling because I have to lock t in the y-axis u can test in i;m giving u the link physiofirst.in/, please suggest somthing

Collapse
fiqihwew profile image
Ahmad Fiqih

hallo iam really being helped with your project
im beginner from indonesia
i have project Creating a Tinder-like Swipe UI on Vue with quasar framework
can i see your full project swipe UI like this with quasar framework?

Collapse
suniluam profile image
sunil-uam

I am creating a similar UI but the swipeable component can take space longer than mobile screen. Since, I need swiping in horizontal direction only, I have locked swipe in vertically. But I need to scroll to view the whole component. If I unlock the vertical interaction, I can only drag around the component but not scroll it. Is there a solution for this?
Tried numerous other libraries but ones that allow scrolling does not have swiping animation.

Collapse
_mariacheline profile image
s h i

Have you ever tried this with Typescript?

Collapse
vycoder profile image
yev Author

Unfortunately, no. But I don't think there would be major issues. Except probably explicitly defining some types for the things you'll use in vue2-interact.

Collapse
_mariacheline profile image
s h i

Thanks for responding to my comment.
I actually followed the blog's tutorial rather than using vue2-interact and I'm having some problems with types.

Collapse
sinisimattia profile image
Mattia Sinisi

Awesome man

Collapse
vycoder profile image
yev Author

Thanks for finding my blabbering useful! I highly appreciate it!

Collapse
angelarae63 profile image
Angela Whisnant

This is fantastic! Proving the point that my instructor made that we don't really need to worry about memorizing all of the html and css stuff. We need to be adept with Google and use our dev communities to find answers for what we are trying to accomplish. I wouldn't classify you as mediocre, though...stealth ninja coder is a better description! :0)

Collapse
vycoder profile image
yev Author

True! It sounds so cliche but really, as long as you have a solid grasp on the fundamentals, you'll find it easier to reason about things which puts you in a better perspective for asking right the questions.

In contrast, I also met people who like to memorize stuff, (some even gatekeep this) but at the end of the day, whatever you methods are, as long as you're productive, I don't think it matters that much.

Thanks for the compliment! (I've always wanted to become a ninja.)