@svelte-put/swipeable

GitHub Github

set up quick swipe gesture action on element

@svelte-put/swipeable @svelte-put/swipeable @svelte-put/swipeable @svelte-put/swipeable

Still on Svelte 4? See the old docs site here.

Installation

npm install --save-dev @svelte-put/swipeable
pnpm add -D @svelte-put/swipeable
yarn add -D @svelte-put/swipeable

Quick Start

<script lang="ts">
	import { swipeable, type SwipeEndEventDetail } from '@svelte-put/swipeable';

	function swipeend(e: CustomEvent<SwipeEndEventDetail>) {
		const { passThreshold, direction } = e.detail;
		if (passThreshold) {
      // do something based on the swipe direction
    }
	}
</script>

<!-- listen for swipe left and swipe right -->
<div use:swipeable style:left="var(--swipe-distance-x)">...</div>

Demo

The following example demonstrates a practical use case for swipeable to implement swipe-to-delete or swipe-to-archive, often seen in notification center or email apps.

Example

Swipe left to archive, swipe right to delete

  • >> Message from Universe


    Lorem ipsum dolor sit amet consectetur adipisicing elit. Repudiandae blanditiis nulla perspiciatis quas necessitatibus deleniti! Sapiente fuge...

  • >> Message from Universe


    Lorem ipsum dolor sit amet consectetur adipisicing elit. Repudiandae blanditiis nulla perspiciatis quas necessitatibus deleniti! Sapiente fuge...

  • >> Message from Universe


    Lorem ipsum dolor sit amet consectetur adipisicing elit. Repudiandae blanditiis nulla perspiciatis quas necessitatibus deleniti! Sapiente fuge...

  • >> Message from Universe


    Lorem ipsum dolor sit amet consectetur adipisicing elit. Repudiandae blanditiis nulla perspiciatis quas necessitatibus deleniti! Sapiente fuge...

<script lang="ts">
	import {
		swipeable,
		type SwipeEndEventDetail,
		type SwipeSingleDirection,
		type SwipeStartEventDetail,
	} from '@svelte-put/swipeable';
	import { slide } from 'svelte/transition';

	const ITEMS = new Array(4).fill(undefined).map(() => ({
		id: crypto.randomUUID(),
		title: 'Message from Universe',
		excerpt:
			'Lorem ipsum dolor sit amet consectetur adipisicing elit. \
			Repudiandae blanditiis nulla perspiciatis quas necessitatibus deleniti! Sapiente fuge...',
	}));

	let items = $state(structuredClone(ITEMS));
	let direction: SwipeSingleDirection | null = $state(null);

	function swipestart(e: CustomEvent<SwipeStartEventDetail>) {
		direction = e.detail.direction;
	}

	function swipeend(e: CustomEvent<SwipeEndEventDetail>) {
		const { passThreshold } = e.detail;
		if (passThreshold) {
			const id = (e.target as HTMLElement).dataset.id;
			items = items.filter((i) => i.id !== id);
		}
	}

	function reset() {
		items = structuredClone(ITEMS);
		direction = null;
	}
</script>

<div class="flex items-baseline justify-between gap-4">
	<p class="mt-0">Swipe left to archive, swipe right to delete</p>
	<button class="c-btn c-btn--outlined" onclick={reset}>Reset</button>
</div>
<ul class="relative mt-8 overflow-hidden border border-current">
	{#each items as { id, title, excerpt } (id)}
		<li
			class="relative"
			class:bg-error-bg={direction === 'right'}
			class:bg-info-bg={direction === 'left'}
			out:slide={{ axis: 'y', duration: 200 }}
		>
			{#if direction === 'right'}
				<div
					class="i i-[trash] absolute left-4 top-1/2 z-0 h-6 w-6 -translate-y-1/2 text-white"
				></div>
			{/if}

			<article
				class="z-px border-outline bg-bg-100 relative touch-pan-x space-y-1 border p-4"
				use:swipeable
				onswipestart={swipestart}
				onswipeend={swipeend}
				style:left="var(--swipe-distance-x)"
				data-id={id}
			>
				<p class="flex items-center gap-2 leading-normal">
					<i class="i i-[envelope-simple] h-6 w-6"></i>
					<span>>></span>
					<span class="font-medium">{title}</span>
				</p>
				<hr />
				<p class="text-sm leading-relaxed">{excerpt}</p>
			</article>

			{#if direction === 'left'}
				<div
					class="i i-[archive] absolute right-4 top-1/2 z-0 h-6 w-6 -translate-y-1/2 text-white"
				></div>
			{/if}
		</li>
	{/each}
</ul>

Events

swipeable fires swipestart when a swipe action in one of the allowed directions is detected (pointermove), and swipeend when the swipe action is completed (pointerup).

interface SwipeStartEventDetail {
	/** direction of this swipe action */
	direction: SwipeSingleDirection;
	/** travel distance of this swipe action in px */
	distance: number;
}
interface SwipeEndEventDetail extends SwipeStartEventDetail {
	/** whether the swipe action passes the threshold, or is a flick */
	passThreshold: boolean;
}

interface SwipeableAttributes {
	onswipestart?: (event: CustomEvent<SwipeStartEventDetail>) => void;
	onswipeend?: (event: CustomEvent<SwipeEndEventDetail>) => void;
}

Multiple swipestart events

A swipestart event might be fired again if user changes the swipe direction during the swipe action. In Demo, try swiping to left and then change to right midway. Observe that the background color and icon are updated accordingly.

Configuration

swipeable takes an optional parameter with the following interface. Details of each property are explained in next sections.

type SwipeableParameter = SwipeableConfig['direction'] | SwipeableConfig | undefined;

interface SwipeableConfig {
	direction?: SwipeDirection | SwipeDirection[];
	threshold?: SwipeThreshold;
	customPropertyPrefix?: string | null;
	followThrough?: SwipeFollowThrough | boolean;
	allowFlick?: boolean | ((ms: number, px: number) => boolean);
	enabled?: boolean;
}

Direction

SwipeableConfig accepts an optional direction property that takes one or an array of directions for swipeable to register at runtime. Supported values are:

type SwipeSingleDirection = 'up' | 'down' | 'left' | 'right';
type SwipeMultiDirection = 'x' | 'y' | 'all';
type SwipeDirection = SwipeSingleDirection | SwipeMultiDirection;

The default is ['left', 'right']. Although it is possible to allow swpieable to listen to all directions, it is recommended to constraint to either horizontal or vertical directions to avoid janky behavior.

Threshold

SwipeableConfigaccepts an optional threshold property that sets up the distance to trigger the swipeend event. The value is a string that takes a number followed by a unit (px, rem, %).

type SwipeThresholdUnit = 'px' | 'rem' | '%';
type SwipeThreshold = `${number}${SwipeThresholdUnit}`;

The default is 30%. Note that percentage is relative to the element size in the travel direction (i.e height for vertical swipe, and width for horizontal swipe).

CSS Custom Property

SwipeableConfig accepts an optional customPropertyPrefix property that sets up the CSS custom property to track the swipe travel distance. Typically you would use this property to shift the element's position following the swipe movement for visual feedback (so that user knows that their swipe is being registered).

<div use:swipeable style:left="var(--swipe-distance-x)"></div>

swipeable tracks displacement via a custom property instead of setting the element's style directly to avoid interfering with user-defined styles, allowing more flexibility.

The default is --swipe, i.e --swipe-distance-x for horizontal swipe, and --swipe-distance-y for vertical swipe. Set to null to disable tracking.

Follow Through

SwipeableConfig accepts an optional followThrough property that instructs swipeable how to behave when swipe reaches the threshold, either:

  • true (default): "follow through" in the swipe direction, then fire swipeend event (as seen in Demo). That is, upon pointerup, the CSS Custom Property will be tweened to element's width / height, or
  • false: stop swipe action immediately and fire swipeend event.

followThrough takes either a boolean or a config object with the following interface.

type SwipeFollowThrough = boolean | {
	/** duration for the follow through animation */
	duration?: number;
	/** easing function for the follow through animation */
	easing?: (t: number) => number;
}

Flick

It is typical to expect a quick swipe with high velocity — a so-called "flick" — to bypass the threshold and be recognized as a complete swipe action (try this in Demo). This can be configured via the SwipeableConfig.allowFlick.

interface SwipeableConfig {
  // ... truncated ...
  allowFlick?: boolean | ((ms: number, px: number) => boolean);
}

For complex configuration, you can provide a function that takes the duration in milliseconds and the distance in pixels, and returns a boolean to indicate whether the swipe should be considered a flick. The default is:

// is flick if the duration is less than 170ms and the velocity is greater than 1px/ms
const DEFAULT_FLICK_CHECK = (ms, px) => ms < 170 && Math.abs(px / ms) > 1;

Happy swiping!

Edit this page on Github