Swipe Navigation/Carousel with Vue [Tutorial]

Swiping horizontally between pages is a very common pattern in native apps, so it makes sense to bring this pattern to your mobile web app or PWA.

There are *a lot* of different ways to do this and I’ve tried many of them, but this has worked the best for me so far. I’m using this method in my hybrid app Imaginarie.

What’s in this tutorial

In this tutorial, we’ll start with a general overview of how this solution works. Then you’ll see a working JSFiddle with thoroughly commented code.

  • What we’re building

What we’re building

We’re building a flexible component for swiping horizontally between pages or items. You can check out the demo, which is best tested on a mobile device.

GIF of JSFiddle demo

Requirements

  • Native feel: Responsive to touch gestures, including quick swipes and longer pans

Tools

  • JavaScript

Alternative solutions: For a simple carousel, there are more lightweight solutions than this, including a horizontal scroll box using scroll-snap.

How it works

Part 1: Displaying items

For this example, we have 6 items in our list. But we don’t want to render all of them in the DOM at once. We only want 3 items: the previous, current, and next items. We’ll figure out the rendered items by keeping track of the index for the current item.

Starting state example:

items: [“red”, “orange”, “yellow”, “green”, “blue”, “purple”]
currentIndex: 4
renderedItems: [“green”, “blue”, “purple”]

To go to the next or previous items, we’ll update currentIndex which will update renderedItems

currentIndex: 5
renderedItems: [“blue”, “purple”, “red”]

To display renderedItems, we’ll create a flexbox container to display them horizontally, with the items centered. Inside, we’ll use v-for to include renderedItems.

Three items rendered at once, but only one currently in view

Important: Each item must have a unique and stable key so that Vue can track each item and avoid re-rendering items as we update the renderedItems list.

Part 2: Adding movement

At this point, we could call our next or previous functions and the item displayed would change on the screen instantly. But our goal is to have the items slide nicely across the screen.

To get a sliding effect, we’ll move the items by translating them left or right by the full width of the screen. To control how fast the items move, we’ll use a CSS transition.

Here’s the sequence of events for going to the next item:

  1. Translate items to the left for a duration of 250ms by applying these CSS properties to our item container:
    transform: translateX(100vw);
    transition: transform 250ms cubic-bezier(0.0, 0.0, 0.2, 1);

Part 3: Handling Touch events

We’re already translating items across the screen, so now we can sync that up with touch gestures.

Here’s the idea: As a user is moving a finger across the screen, translate the items by the same amount. Then when they lift their finger, continue the movement across the screen.

Items translated a bit to the left to match gesture to go “next”

Setting up Hammer.JS

This tutorial uses the popular open-source library Hammer to respond to touch events. It’s not totally without issues in my experience, but it handles a lot of the cross-browser complexity. This Google Developers tutorial on handling touch has great info if you want to avoid another library.

To get set up, we’ll create a new Hammer instance on a parent div and tell it to only look for horizontal swipe and pan gestures. Then we’ll add an event listener to respond to those gestures.

During a gesture, events are fired with a lot of info, but we’re only using three data points:

  • deltaX: The total distance traveled on the X-axis. Positive numbers mean left-to-right (previous), and negative numbers right-to-left (next)

Responding to events

Now we can react to quick swipe and longer pan gestures:

  • During a gesture, translate the items the same distance and direction as deltaX

Keeping transitions smooth & responsive

For translating during a gesture, we want the transition to be instant and responsive. But when we finish the translation on gesture end, we want it to be smooth, with a duration and timing property applied. You’ll see in the code that we’ll be updating the transition class on the fly.

Part 4: Handling additional scenarios

At this point, our component would work OK, but it would be pretty buggy. So we have a few scenarios to solve for:

  • Changing direction: What if a user starts a gesture going one direction, but then goes back the other way? In that scenario, we’ll assume the user wants to stay on the current item so we’ll reset it back to where it started.

Part 5: Adding tap targets (Optional)

For my app, I added another way to change slides: tapping the left or right edges of the screen (like on a Kindle). Implementing this is just a matter of adding two non-visible divs on the edges of the screen and adding click handlers to call the previous and next functions.

Part 6: Adding edge effect for the end of the list (Optional)

What happens when a user tries to go to the next item when they’ve reached the end (or the previous item when they’re on the first)? Either the cards can go in an infinite loop, or we can stop the user from going forward or back when they reach one of the ends.

You can decide which option makes more sense for your use case, so in the example, the property isInfiniteLoop controls this setting.

Edge shape expanded

For the linear option, it’s a good practice to indicate that they’ve reached the end. There are multiple ways to do this, but I chose to replicate the common pattern of showing a curved overlay that comes into view as a user tries to swipe (not sure what it’s called).

To do this, we’ll add SVG curve shapes to each side of the screen with a starting scale of 0. We’ll keep track of whether we’re at one of these end cards and if we are we’ll intercept our gesture handler. Instead of translating the items like normal, we’ll increase the scale of the shape so it comes into view.

The code

Here’s the complete example code in JSFiddle or a Codepen if you prefer.

The demo is pretty comprehensive, but if you’re curious to see a live example using Vuex, Vue Router, etc here’s a gist for the swipe component for Imaginarie.

Accessibility

For people using a keyboard or assistive technology to navigate, gestures likely aren’t going to be very accessible. In this example, I made the left and right tap targets accessible to a keyboard and labeled them as buttons. The bold attributes below were added for accessibility:

<div class="touch-tap-right" role="button" aria-label="Next" tabindex="0" @click="next" @keyup.enter="next" @keyup.space="next">

This does introduce a slight problem for our design though. For items that are interactive, like buttons and input fields, browsers will automatically add an outline when they are active (i.e. a blue or gold blurred outline). This means that when we swipe or tap we’ll see an unexpected outline around the left or right sides of the screen.

So we don’t want the outline when we swipe, tap, or click, but we do want it for someone using a keyboard to navigate. This feature is only partially implemented across browsers, so in the meantime, I’m using a :focus-visible polyfill in my app. For this example, I just turned off the outline altogether, which isn’t recommended.

Also, for users who prefer fewer animations, the CSS includes the media query prefers-reduced-motion to turn off all translation effects.

Variations & options

There are a lot of ways to customize this method and make it fit more use cases. Here a few ideas:

  • Add progress dots: For a finite number of items, it can be nice to show users where they are in the set

Thanks for reading!

Please let me know if you have any questions, find a mistake, or have ideas on how to improve. And give some claps if you found this useful!

People-focused developer & designer — portfolio at saturdaydesign.dev

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store