DEV Community

Cover image for Animated card slider with Vue & GSAP - WotW
Eder Díaz
Eder Díaz

Posted on • Edited on

Animated card slider with Vue & GSAP - WotW

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 and looks like this:

slider

Preparations

Similarly to last widget, today's widget we will be using vue.js for the interactions, and 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:

<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>
Enter fullscreen mode Exit fullscreen mode

Styling!

Right now it doesn't look anything close to the final product. First I'll simulate the mobile viewport with this rule:

.slider {
  overflow: hidden;
  background-color: #1F1140;
  width: 360px;
  height: 640px;
}
Enter fullscreen mode Exit fullscreen mode

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.

.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)
}
Enter fullscreen mode Exit fullscreen mode

We're getting closer
halfway

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:

.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;
}
Enter fullscreen mode Exit fullscreen mode

style done
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:

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'
      }
    ]
  }
})
Enter fullscreen mode Exit fullscreen mode

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:

  data: {
    // ... slide data
    selectedIndex: 0
  },
  computed: {
    selectedSlide () {
      return this.slides[this.selectedIndex]
    }
  }
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

almost finished
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.

<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 -->
Enter fullscreen mode Exit fullscreen mode

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

  data: {
    // ... other variables
    dragging: false,
    initialMouseX: 0,
    initialCardsX: 0,
    cardsX: 0
  },
  methods: {
    startDrag (e) {

    },
    stopDrag () {

    },
    mouseMoving (e) {

    }
  }
Enter fullscreen mode Exit fullscreen mode

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:

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

}
Enter fullscreen mode Exit fullscreen mode

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:

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

  }
}
Enter fullscreen mode Exit fullscreen mode

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:

startDrag (e) {
  this.dragging = true
  this.initialMouseX = e.pageX
  this.initialCardsX = this.cardsX
},
stopDrag () {
  this.dragging = false
},
mouseMoving (e) {
  if(this.dragging) {

  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

...
<div class="slider-cards" :style="`transform: translate3d(${cardsX}px,0,0)`">
...
Enter fullscreen mode Exit fullscreen mode

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.

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:

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})
}
Enter fullscreen mode Exit fullscreen mode

To understand better that formula, this gif shows how the cardsX value correlates to the nearestSlide.
nearestSlide

And now the final result!

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

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

If you haven't checked the previous one, here it is.

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

Top comments (3)

Collapse
 
vj_andrei profile image
Andreas Koutsoukos

Great post! I made some fork for mobile codesandbox.io/s/card-animation-znm1o

Collapse
 
givsta profile image
Given M

Thank you for this Eder, schooled me! :-)

Collapse
 
corentinn profile image
Corentin Noirot

I'll try it, thank you !