Fluid Swipe-to-Action
A list item that can be dragged horizontally with high elasticity. Dropping it reveals under-the-fold action buttons (like Delete/Archive) with an elastic rubber-band snap.
Use
Mobile-first list views (Emails, Tasks)
Avoid
Desktop grids where a mouse hover menu is vastly superior and standard
Safer Alternative
No safer default listed.
Risk Level
Low risk
Live Demo
Design Sync
10:30 AMSwipe me left to reveal the delete action permanently.
What It Is
A list item that can be dragged horizontally with high elasticity. Dropping it reveals under-the-fold action buttons (like Delete/Archive) with an elastic rubber-band snap.
✓When to Use
- Mobile-first list views (Emails, Tasks)
- Heavy editing interfaces
✕When NOT to Use
- Desktop grids where a mouse hover menu is vastly superior and standard
Configuration Tips
- 01Map Framer Motion `drag="x"` coordinates to an elastic spring, scaling the revealed icons based on drag distance.
You've Seen It In
AI Implementation Prompts
Move from a fast scaffold to production details, then tune timing and edge states.
Build a mobile-style list item that can be dragged left using framer-motion drag constraints, revealing a colored Delete background underneath.
Reference Implementation
The same implementation approach used by the live preview, kept compact enough to inspect and adapt.
"use client";
import { motion, useMotionValue, useTransform } from 'framer-motion';
import { useState, useEffect } from 'react';
export function LiquidSwipeActionDemo({ isPlaying = false }: { isPlaying?: boolean }) {
const x = useMotionValue(0);
const [isDeleted, setIsDeleted] = useState(false);
const [isAutoAnimating, setIsAutoAnimating] = useState(false);
"text-stone-400 italic">// Dynamic styles based on drag distance
const deleteOpacity = useTransform(x, [0, -60, -120], [0, 1, 1]);
const deleteScale = useTransform(x, [0, -60], [0.5, 1]);
"text-stone-400 italic">// Auto play when `isPlaying` (like hovering over the grid item)
useEffect(() => {
if (!isPlaying) {
x.set(0);
setIsDeleted(false);
setIsAutoAnimating(false);
return;
}
setIsAutoAnimating(true);
let time = 0;
const loop = setInterval(() => {
time += 0.05;
"text-stone-400 italic">// Simulate a swipe left then release
if (time < Math.PI) {
x.set(-Math.abs(Math.sin(time)) * 140);
} else if (time >= Math.PI && time < 4.5) {
"text-stone-400 italic">// rest
x.set(0);
} else {
time = 0; "text-stone-400 italic">// reset
}
}, 30);
return () => clearInterval(loop);
}, [isPlaying, x]);
const handleDragEnd = () => {
if (x.get() < -110) {
"text-stone-400 italic">// Trigger delete
setIsDeleted(true);
setTimeout(() => {
setIsDeleted(false);
x.set(0);
}, 2000);
}
};
return (
<div className="flex h-64 w-full items-center justify-center overflow-hidden rounded-2xl bg-stone-100 p-4 shadow-sm ring-1 ring-stone-200 relative">
<div className="w-full max-w-sm rounded-xl overflow-hidden relative border border-stone-200 bg-red-500 shadow-sm">
{"text-stone-400 italic">/* The Action Background (Red Delete) */}
<div className="absolute inset-y-0 right-0 w-1/2 flex items-center justify-end pr-6">
<motion.div
style={{ opacity: deleteOpacity, scale: deleteScale }}
className="flex flex-col items-center justify-center text-white"
>
<svg className="w-6 h-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</motion.div>
</div>
{"text-stone-400 italic">/* The Draggable List Item */}
<motion.div
drag={isAutoAnimating ? false : "x"}
dragConstraints={{ left: -140, right: 0 }}
dragElastic={0.4}
whileTap={isAutoAnimating ? {} : { cursor: "grabbing" }}
onDragEnd={handleDragEnd}
style={{ x }}
animate={isDeleted ? { x: -500, opacity: 0 } : (isAutoAnimating ? {} : { opacity: 1 })}
transition={isDeleted ? { duration: 0.3 } : { type: "spring", stiffness: 300, damping: 25 }}
className="w-full bg-white px-6 py-4 flex items-center gap-4 cursor-grab shadow-[2px_0_10px_rgba(0,0,0,0.1)] relative z-10"
>
<div className="w-12 h-12 bg-stone-100 rounded-full flex-shrink-0 flex items-center justify-center text-stone-400">
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div className="flex flex-col w-full">
<div className="flex justify-between w-full mb-1">
<h4 className="font-bold text-stone-900 text-sm">Design Sync</h4>
<span className="text-xs text-stone-400">10:30 AM</span>
</div>
<p className="text-xs text-stone-500 line-clamp-1 h-4">Swipe me left to reveal the delete action permanently.</p>
</div>
</motion.div>
</div>
</div>
);
}Related Effects
Pull-to-Refresh
A gesture-driven reveal of a loading indicator when a user scrolls past the top edge of a content area, triggering a data refresh.
Drag & Drop Ghost
When dragging an item, the original item remains lowered in opacity as a placeholder while a slightly scaled-up, shadowed ghost follows the pointer.