@svelte-put/swipeable
GithubCompatible with or powered directly by Svelte runes.
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.
<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
SwipeableConfig
accepts 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 fireswipeend
event (as seen in Demo). That is, uponpointerup
, the CSS Custom Property will be tweened to element's width / height, orfalse
: stop swipe action immediately and fireswipeend
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!