Animated card slider with Vue & GSAP - WotW
Tags: webdev, vue, tutorial, wotw
Published: 2018-04-18 14:51:03

---
title: Animated card slider with Vue & GSAP - WotW
published: true
description: How I made an image slider based on a dribble gif
tags: ['webdev', 'vue', 'tutorial', 'wotw']
cover_image: https://thepracticaldev.s3.amazonaws.com/i/dx9dw7rbi817zh00myv7.jpeg
---

This is the third installment of the **Widget of the Week** series.

Today I'll show you the process to make a styled **card slider** from scratch using Vue.

The inspiration for this widget is [this](https://uimovement.com/ui/5332/ticket-booking/) and looks like this:

![slider](https://thepracticaldev.s3.amazonaws.com/i/mrv7rrg33h7hl0h6ltgp.gif)


### Preparations

Similarly to [last widget](https://dev.to/ederchrono/making-a-submit-button-with-loader---wotw-2ma), today's widget we will be using [vue.js](https://vuejs.org/) for the interactions, and [tweenlite](https://greensock.com/tweenlite) for animations.

### The HTML structure

Basically the elements of the slider are the **cards** and the **info container**, I'll start by adding them along with some classes to be able to style them in the next step:

``` html
<div id="slider" class="slider">
  <div class="slider-cards">
    <div class="slider-card"></div>
    <div class="slider-card"></div>
    <div class="slider-card"></div>
  </div>
  <div class="slider-info">
    <h1>Title</h1>
    <p>description</p>
    <button>Action</button>
  </div>
</div>
```

### Styling!

Right now it doesn't look anything close to the final product. First I'll simulate the mobile viewport with this rule:
``` css
.slider {
  overflow: hidden;
  background-color: #1F1140;
  width: 360px;
  height: 640px;
}
```
For the cards, I'll be using a margin in the container to center the first card, then the cards will separate each other with a right margin. Also we need the cards container to be relative and have a z-index to be on top of the `slider-info` div. 

The cards should be `inline` so they can be aside of each other, but for that too work, the container should be wide enough. Each card in this case is roughly 300px wide, so the container will be 900px wide because we have 3 cards (in case we had more cards we would need to calculate the total width needed). 

Lastly we will add a box shadow to give the impression that the card floats.

``` css
.slider-cards {
  position: relative;
  width: 900px;
  margin: 20px 50px;  
  z-index: 1;
}
.slider-card {
  display: inline-block;
  background-color: grey;
  overflow: hidden;
  width: 260px;
  height: 360px;
  margin-right: 30px;
  border-radius: 12px;
  box-shadow:0px 60px 20px -20px rgba(0, 0, 0, 0.3)
}
```
We're getting closer
![halfway](https://thepracticaldev.s3.amazonaws.com/i/eb91zo2auxjb47ct89cn.png)

Now it's the turn of the `slider-info` to get its makeover. We will add a background color, dimensions and margins to centered the info. 

It is important that it overlaps with the cards container, in order to do that, the `margin-top` will be negative and to compensate we add some `padding-top`. 

We need to make sure that the `overflow` property is *hidden* to make the button at the bottom have the same rounded corners as the info container. After that is just a matter of styling the title, description and the button in the following way:

``` css
.slider-info {
  position: relative;
  overflow: hidden;
  background-color: white;
  margin-top: -200px;
  margin-left: 30px;
  padding: 200px 20px 0;
  width: 260px;
  height: 200px;
  text-align: center;
  border-radius: 8px;
}
.slider-info h1 {
  font-family: Arial Black, Gadget, sans-serif;
  line-height: 25px;
  font-size: 23px;
}
.slider-info p {
  font-family: Arial, Helvetica, sans-serif;
}
.slider-button {
  position: absolute;
  width: 100%;
  height: 50px;
  bottom: 0;
  left: 0;
  border: none;
  color: white;
  background-color: #E71284;
  font-size: 18px;
  font-family: Arial, Helvetica, sans-serif;
}
```
![style done](https://thepracticaldev.s3.amazonaws.com/i/lp48mdalohqksk75j6et.png)
Much better.

### Filling with data

We are ready to start using Vue, let's create an instance and also set some data from [The Movie DB](https://www.themoviedb.org/):

``` js
new Vue({
  el: '#slider',
  data: {
    slides: [
      {
        title: 'Ready Player One',
        description: 'When the creator of a popular video game system dies, a virtual contest is created to compete for his fortune.',
        image: 'https://image.tmdb.org/t/p/w300_and_h450_bestv2/pU1ULUq8D3iRxl1fdX2lZIzdHuI.jpg'
      },
      {
        title: 'Avengers: Infinity War',
        description: 'As the Avengers and their allies have continued to protect the world from threats too large for any...',
        image: 'https://image.tmdb.org/t/p/w300_and_h450_bestv2/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg'
      },
      {
        title: 'Coco',
        description: 'Despite his family’s baffling generations-old ban on music, Miguel dreams of becoming an accomplished musician...',
        image: 'https://image.tmdb.org/t/p/w300_and_h450_bestv2/eKi8dIrr8voobbaGzDpe8w0PVbC.jpg'
      }
    ]
  }
})
```

Now to be able to show the data, we need to define the default selected movie. That can be accomplished with another variable in our data called `selectedIndex` and a computed property that can give us the data from the slides according to that selected index:

``` js
  data: {
    // ... slide data
    selectedIndex: 0
  },
  computed: {
    selectedSlide () {
      return this.slides[this.selectedIndex]
    }
  }
```

Then in our template we will bind the cards with a `v-for`, and the info to the corresponding data:

``` html
<div id="slider" class="slider">
  <div class="slider-cards">
    <div 
         v-for="(slide, index) in slides" 
         :key="index"
         class="slider-card">
      <img :src="slide.image" :alt="slide.title">
    </div>
  </div>
  <div class="slider-info">
    <h1>{{selectedSlide.title}}</h1>
    <p>{{selectedSlide.description}}</p>
    <button class="slider-button">BOOK</button>
  </div>
</div>
```
![almost finished](https://thepracticaldev.s3.amazonaws.com/i/1t6aga9fq6qop3i4b32t.png)
This looks almost finished, at least aesthetically, but we still need...

### The interactions

If we breakdown the interactions for the slider, they are basically 3, when we press down the card, moving the card and letting the card go. To track those actions we will need to bind `@mouseDown`, `@mouseUp` and `@mouseMove` to methods inside the Vue instance. Also to prevent the images to *ghost* they should have the property `draggable=false`.

``` html
<div id="slider" class="slider" @mouseMove="mouseMoving">
  <div class="slider-cards">
    <div @mouseDown="startDrag"
         @mouseUp="stopDrag"
         v-for="(slide, index) in slides" 
         :key="index"
         class="slider-card">
      <img :src="slide.image" :alt="slide.title" draggable="false">
    </div>
  </div>
  <!-- slider info and the rest -->
```

Now we need to create those methods in the Vue side, also we'll add a couple of variables inside our data object:

``` js
  data: {
    // ... other variables
    dragging: false,
    initialMouseX: 0,
    initialCardsX: 0,
    cardsX: 0
  },
  methods: {
    startDrag (e) {
      
    },
    stopDrag () {
      
    },
    mouseMoving (e) {
      
    }
  }
```
All three methods receive an event (in this case we call it `e`) but we will just need it in the `startDrag` and `mouseMoving` methods.
On the next code snippets I'll break down the process step by step to fill those 3 methods, so I'll ignore the rest of the code.

First we need to set `dragging` to *true* or *false* depending on the mouse actions:

``` js
startDrag (e) {
  this.dragging = true
},
stopDrag () {
  this.dragging = false
},
mouseMoving (e) {
  
}
```
Pretty straightforward, now we want to only be able to move our cards if we are dragging them, so inside the `mouseMoving` method we will add this conditional:

``` js
startDrag (e) {
  this.dragging = true
},
stopDrag () {
  this.dragging = false
},
mouseMoving (e) {
  if(this.dragging) {

  }
}
```

Alright now things will get interesting, we need to track what's the position of both the cards and the mouse when we start dragging, the `pageX` property will tell us about the mouse position, and the `cardsX` inside our data will be tracking the cards' container position:
``` js
startDrag (e) {
  this.dragging = true
  this.initialMouseX = e.pageX
  this.initialCardsX = this.cardsX
},
stopDrag () {
  this.dragging = false
},
mouseMoving (e) {
  if(this.dragging) {

  }
}
```
After storing the initial X for cards and mouse, we can deduct the target position of the cards' container by calculating the mouse position difference when the `mouseMoving` method executes like this:
``` js
startDrag (e) {
  this.dragging = true
  this.initialMouseX = e.pageX
  this.initialCardsX = this.cardsX
},
stopDrag () {
  this.dragging = false
},
mouseMoving (e) {
  if(this.dragging) {
    const dragAmount = e.pageX - this.initialMouseX
    const targetX = this.initialCardsX + dragAmount
    this.cardsX = targetX
  }
}
```
Our component is almost ready to move, we just need to find a way to bind the cards' container to the `cardsX` property, this can be done adding this property to the HTML:
``` html
...
<div class="slider-cards" :style="`transform: translate3d(${cardsX}px,0,0)`">
...
```
You might ask "Why are you using translate3d instead of just a regular 2d translate?", the reason is that `translate3d` is *hardware accelerated*, and most of the times has a better performance. You can check by yourself in [this site](https://jsperf.com/translate3d-vs-xy).

The slides are moving now, but there's one little problem, when we let go they stay wherever we drop them, also the movie info is not changing. What we actually need is for them to find what's the nearest slide and center it. 

To find the nearest slide we just need to divide the current position with the width of the card and round the result. Then with TweenLite we will animate the cards to the corresponding position:

``` js
stopDrag () {
  this.dragging = false

  const cardWidth = 290
  const nearestSlide = -Math.round(this.cardsX / cardWidth)
  this.selectedIndex = Math.min(Math.max(0, nearestSlide), this.slides.length -1)
  TweenLite.to(this, 0.3, {cardsX: -this.selectedIndex * cardWidth})
}
```
To understand better that formula, this gif shows how the `cardsX` value correlates to the `nearestSlide`.
![nearestSlide](https://media.giphy.com/media/jaP77e4YAxcZgmPSBn/giphy.gif)


And now the final result!

{% codepen https://codepen.io/ederdiaz/pen/LmPwez %}

Right now it only works on desktop devices, but that could probably be fixed with `vue-touch`, you can learn more about it in [this article](https://alligator.io/vuejs/vue-touch-events/)

That’s it for the 3rd **Widget of the Week**.

If you haven't checked the previous one, [here it is](https://dev.to/ederchrono/making-a-submit-button-with-loader---wotw-2ma).

Also if you want to see a specific widget for next week, post it in the comments section.