Swipe Navigation/Carousel with Vue [Tutorial]

Elena Czubiak
8 min readFeb 19, 2020

--

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
  • How it works
  • The code
  • Accessibility
  • Variations & options

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
  • Lazy: Render as few items in the DOM as possible, so the total number of items doesn’t affect performance
  • Programmatic: With every swipe, we can update the route, send data to Analytics, etc.

Tools

  • JavaScript
  • Vue, but could be easily adapted for React
  • Hammer.JS touch library
  • Lodash debounce function or equivalent

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);
  2. We’ll listen for an event that tells us the transition is done. Then, we’ll change currentIndex so that the next item becomes the current one and a new next item is rendered

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)
  • deltaY: Distance on the Y-axis
  • isFinal: Whether the gesture is over (finger or mouse lifted)

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
  • When the gesture is over, call the previous or next functions based on the direction, which will translate the item the rest of the way off the screen

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.
  • Vertical gestures: We only want to respond to gestures that are clearly horizontal, so we’ll ignore gestures that are more vertical. But if a gesture starts horizontally and then goes diagonal, we’ll continue to react to the horizontal distance.
  • Too-frequent interactions: Things can get complicated if someone is swiping or tapping super quickly. The most straightforward way to deal with this is to not respond to taps or gestures while an item is still transitioning across the screen. Luckily this transition happens quickly enough (250ms) that it should still feel very responsive even if you’re swiping through quickly.
  • Duplicate events: Sometimes gestures will fire duplicate events, so we don’t want to end up skipping multiple slides at once. To avoid this, we’re debouncing our next and previous functions so that they are only called at most every 100ms.

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
  • Add buttons for larger screens: In my app, I keep the cards the same width on larger screens but add arrow buttons for navigating
  • Show more than one item at a time: For larger screens or smaller items, you may want to show multiple items at once. To do this, you would need to update the logic for displaying items and how far you’re translating the items when going to the next or previous items

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!

--

--