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.
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.
- Detect if the card was dragged out of view and hide it.
- Stack the draggable cards on top of each other.
- 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.
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-events
on 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:
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).
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>
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.
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 div
s. 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:
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).
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
- Kittynder Demo
- Kittynder Project Repository
- Swipeable Card Stack Using Vue
- vue2-interact
- vue2-interact Documentation
This article is originally posted on my personal website.
Top comments (13)
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.
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?
Have you ever tried this with Typescript?
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
.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.
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
Awesome man
Thanks for finding my blabbering useful! I highly appreciate it!
Hello sir @vycoder , boss I want to use this to my project but how can I do it in vue3 I can't get it to work boss. Salamat po! :)
How can I applied it in my tinder resource tindermodapk.pro/ with accurate way?
How can I use this on my Tinder profile in the right way?
tindermodapk.download/tinder-for-pc/
Some comments may only be visible to logged-in visitors. Sign in to view all comments.