Building a swipeable card stack with interact.js and Svelte
Tags: svelte, javascript, css, html
Published: 2019-10-07 17:48:03

---
title: Building a swipeable card stack with interact.js and Svelte
published: true
date: 2019-10-07 18:35:00 UTC
tags: svelte, javascript, css, html
canonical_url: https://smth.uk/building-a-swipeable-card-stack/
cover_image: https://smth.uk/img/36q.jpg
---

I've been waiting for an opportunity to dip my toe into some [Svelte](https://svelte.dev/) for a while. Faced with a little free time, I decided to create that opportunity. For anyone who hasn't heard of Svelte, it is a JavaScript / component framework, along the lines of React and Vue, but with an added compile step at build time. So what did I decide to use it for? Inspired by [this post by Mateusz Rybczonek](https://css-tricks.com/swipeable-card-stack-using-vue-js-and-interact-js/), I set myself the challenge of building a swipeable card stack interface. You can see the result [here](https://36q.app/).

In this article I will explain the steps I took in building the above interface, and detail some of the approaches I took.

## Step 1: Sapper

I really like static site generators (SSG), and will usually reach for one if a project has static content (such as this one). Fortunately there is a Svelte based SSG; its called [Sapper](https://sapper.svelte.dev/). The [Sapper template](https://github.com/sveltejs/sapper-template) makes a pretty good starting point for a project like this, and comes in Rollup and Webpack variants. I went for Rollup, getting up and running like so:

```bash
npx degit "sveltejs/sapper-template#rollup" my-app
cd my-app
npm install
npm run dev
```

There were a few things in this template that I didn't need, which were either deleted or repurposed. The `about` and `blog` routes were removed, but not before repurposing `blog/_posts.js`, `blog/index.json.js` and `blog/index.svelte` to deliver the content for my app.

I used the include `Nav` component as a guide to creating my first Svelte component, the only component in this app. I'll get back to that in a moment.

## Step 2: (optional) PostCSS

I like processing my styles with [PostCSS](https://postcss.org/), I tend to use [preset-env](https://preset-env.cssdb.org/) to enable nesting and autoprefixing. I used [this Tailwind template](https://github.com/langbamit/sapper-postcss-tailwind-rollup/commit/9d436a85530fca654882c8f9ece8ace7778aa22b) as a guide to set this up with Sapper. Installing the required/desired packages, editing the Rollup config, and importing the CSS file into `server.js`.

```bash
npm install --save-dev postcss postcss-import rollup-plugin-postcss svelte-preprocess postcss-preset-env cssnano
```

```js
// rollup.config.js
// ...
import getPreprocessor from 'svelte-preprocess';
import postcss from 'rollup-plugin-postcss';
import path from 'path';
// ...
const postcssPlugins = [
	require("postcss-import")(),
	require("postcss-preset-env")({
    features: {
      'nesting-rules': true
    }
  }),
	require("cssnano")()
]
const preprocess = getPreprocessor({
	transformers: {
		postcss: {
			plugins: postcssPlugins
		}
	}
});
// ...
export default {
	client: {
	  // ...
		plugins: [
			postcss({extract: true}),
			svelte({
				// ...
				preprocess
			}),
			// ...
		],
		// ...
	},
	server: {
		// ...
		plugins: [
			// ...
      postcss({
				plugins: postcssPlugins,
				extract: path.resolve(__dirname, './static/global.css')
			})
		],
		// ...
	},
	// ...
};
```

(Add styles to `src/css/main.css`)

```js
// src/server.js
// ...
import './css/main.css';
// ...
```

Its worth noting that using this particular approach means you won't be taking advantage of Sapper's code splitting when it comes to CSS, but given that this would be a single page app, I didn't see that as being an issue.

## Step 3: Creating the Card component

There will be multiple cards in this interface, so it makes sense to create a component for them. This simply needs to be a template with some props, like so:

```html
<!-- components/Card.svelte -->
<script>
	export let isCurrent;
	export let cardContent;
</script>

<p class="card" data-dragging="false" data-status="{isCurrent === true ? 'current' : 'waiting'}">
	<span class="card_content">{cardContent}</span>
</p>
```

I've given the card a class so it can be styled as such, plus a couple of data attributes to hold some contextual information that will become useful later. All three attributes could be handled with classes, but I like to use a different syntax for contextual stuff to [make my CSS easier to read](https://c3css.com/). You might also think that the JavaScript to handle the dragging etc should live in this file. When I tried this I found that the script would run for each instance of the component (which is not what I wanted). There's probably a way of making it behave as I wanted, but as I had a layout template not really being used for much, I decided to put all the logic there.

If you were writing your CSS inside the component, it would live in a `style` tag within this file. My CSS lives in a good old CSS file. Its pretty simple so I won't go over it here. Essentially I have a fixed size `card` component, absolutely positioned.

## Step 4: Putting your cards on the table

In `index.svelte` I add instances of the `Card` component to the page. As mentioned earlier, I made use of the blog code to store the content of each card in an array, which I then iterated over like so:

```html
{#each cards as card, i}
	<Card cardContent={card.content} isCurrent={i === 0}/>
{/each}
```

Setting `isCurrent` to `true` for the first item in the array. For simplicity you might just want to put the cards directly into this page:

```html
<Card cardContent={"One"} isCurrent={true}/>
<Card cardContent={"Two"} isCurrent={false}/>
<Card cardContent={"Three"} isCurrent={false}/>
```

In either case, you also need to import the component into the page:

```html
<script>
	import Card from '../components/Card.svelte';
</script>
```

## Step 5: Draggable cards

Now for the fun stuff, the interactivity. I put all the interactivity logic in my `_layout.svelte` file, which until this point was pretty much empty. The dragging relies on [interact.js](https://interactjs.io/) which we need to add to our project before importing into our template.

```bash
npm install --save-dev interactjs
```

The basis for the below code is the dragging example given on the interact.js website. The alterations and additions I will outline here. First thing to note is in Svelte anything that relies on the DOM being ready goes inside an `onMount` function. To use this function, we first need to `import { onMount } from 'svelte'`. I took the concept of "interact threshold" and how that relates to rotation from Mateusz Rybczonek's article. `interactThreshold` represents how far a card needs to be dragged before it is considered dismissed. The interact.js example stores the draggable objects position in data attributes, and adds inline styles to transform its position. Preferring to keep the styles in the style sheet, I used CSS custom properties to store these variables, which are referenced in the CSS. To access the custom properties in the JavaScript, I used [Andy Bell's `getCSSCustomProp` function](https://hankchizljaw.com/wrote/get-css-custom-property-value-with-javascript/#heading-the-getcsscustomprop-function). Finally, inside the `onend` function, we check whether the card has moved a sufficient amount to dismiss. If so we remove its `current` status and give it to the next card. We also move it off the screen to the left or right, depending on whether its `x` coordinate is positive or negative. If the card has not moved a sufficient amount, we reset its position and rotation custom properties.

```html
<script context="module">
	import interact from "interactjs";
</script>

<script>
	import { onMount } from 'svelte';

	const interactThreshold = 100;
	const interactMaxRotation = 15;

	let rotation = 0;
	let x = 0;
	let y = 0;

	// https://hankchizljaw.com/wrote/get-css-custom-property-value-with-javascript/#heading-the-getcsscustomprop-function
	const getCSSCustomProp = (propKey, element = document.documentElement, castAs = 'string') => {
		let response = getComputedStyle(element).getPropertyValue(propKey);

		// Tidy up the string if there's something to work with
		if (response.length) {
			response = response.replace(/\'|"/g, '').trim();
		}

		// Convert the response into a whatever type we wanted
		switch (castAs) {
			case 'number':
			case 'int':
				return parseInt(response, 10);
			case 'float':
				return parseFloat(response, 10);
			case 'boolean':
			case 'bool':
				return response === 'true' || response === '1';
		}

		// Return the string response by default
		return response;
	};

	function dragMoveListener (event) {
		var target = event.target

		// keep the dragged position in the custom properties
		x = (getCSSCustomProp('--card-x', target, 'float') || 0) + event.dx
		y = (getCSSCustomProp('--card-y', target, 'float') || 0) + event.dy

		// add rotation based on card position
		rotation = interactMaxRotation * (x / interactThreshold);
		if (rotation > interactMaxRotation) rotation = interactMaxRotation;
		else if (rotation < -interactMaxRotation) rotation = -interactMaxRotation;

		// update styles
		target.style.setProperty('--card-x', x + 'px');
		target.style.setProperty('--card-y', y + 'px');
		target.style.setProperty('--card-r', rotation + 'deg');
	}

	onMount(() => {
		// get viewport width
		const vw = document.documentElement.clientWidth;
		// create an off canvas x coordinate
		let offX = 400;
		if (vw > 400) {
			offX = vw;
		}

		// interact.js
		interact('.card[data-status="current"]:not(:last-child)').draggable({

			onstart: () => {
				// signify dragging
				event.target.setAttribute('data-dragging', true);
			},

			// call this function on every dragmove event
			onmove: dragMoveListener,

			// call this function on every dragend event
			onend: (event) => {
				// signify dragging stopped
				event.target.setAttribute('data-dragging', false);

				// calculate how far card moved
				let moved = (Math.sqrt(Math.pow(event.pageX - event.x0, 2) + Math.pow(event.pageY - event.y0, 2) | 0));

				if (moved > interactThreshold) {
					// remove card
					event.target.setAttribute('data-status', "done");
					if (x > 0) {
						x = offX;
					} else {
						x = (offX * -1);
					}
					// activate next card
					event.target.nextElementSibling.setAttribute('data-status', 'current');
				}
				else {
					// reset vars
					x = 0;
					y = 0;
					rotation = 0;
					// update rotation
					event.target.style.setProperty('--card-r', rotation + 'deg');
				}
				// update x and y pos
				event.target.style.setProperty('--card-x', x + 'px');
				event.target.style.setProperty('--card-y', y + 'px');
			}
		});
	});

</script>

<main class="container">
	<slot></slot>
</main>
```

That's a big chunk of code, but pretty self explanatory I hope.

## Step 6: Details and finessing

With the functionality in place, there remains some refining to do. For example, you're probably going to want to include some transitions in your CSS, to make the moving and rotations smooth. An important point to consider is that having a transition on the card while it is being dragged will cause problems. That's why we added the `data-dragging` attribute that is toggled to true when a card is being dragged. It means you can safely add something like this to your CSS:

```css
.card[data-dragging="false"] {
	transition: transform 0.5s;
}
```

I also added a small rotation to the next card in the stack, to indicate that there is a card below. There are many ways you could design this though, I'll leave that to you.