35. Swipe to Delete
Framer Motion
This is quite a complicated example. A lot is going on here.
A few details in the Item()
component:
- When the dragging stops (
onDragEnd()
), thehandleDragEnd()
function looks at the currentoffset
orvelocity
. - When one of those is high enough:
- an
useAnimation()
animation will slide the item offscreen, - and the
onDelete()
function in the parent component is called.
- an
- The items have a
layout
property and springytransition
settings, so that they animate to their new position automatically.
const initialItems = [0, 1, 2, 3, 4]
const height = 70
const padding = 10
const size = 150
function Item({ total, index, onDelete }) {
const controls = useAnimation()
async function handleDragEnd(event, info) {
const offset = info.offset.x
const velocity = info.velocity.x
if (offset < -100 || velocity < -500) {
await controls.start({
x: "-100%",
transition: { duration: 0.2 },
})
onDelete(index)
} else {
controls.start({ x: 0, opacity: 1, transition: { duration: 0.5 } })
}
}
return (
<motion.div
style={{
width: 150,
height: height,
borderRadius: 20,
overflow: "hidden",
marginBottom: total - 1 === index ? 0 : 10,
willChange: "transform",
cursor: "grab",
}}
whileTap={{ cursor: "grabbing" }}
layout
transition={{
type: "spring",
stiffness: 600,
damping: 30,
}}
>
<motion.div
style={{
width: size,
height: height,
borderRadius: 20,
backgroundColor: "#fff",
}}
drag="x"
dragDirectionLock
onDragEnd={handleDragEnd}
animate={controls}
/>
</motion.div>
)
}
export function FM35SwipeToDelete() {
const y = useMotionValue(0)
const [items, setItems] = React.useState(initialItems)
const { top, bottom } = useConstraints(items)
const controls = useAnimation()
const totalScroll = getHeight(items)
const scrollContainer = 150
function onDelete(index) {
const newItems = [...items]
newItems.splice(index, 1)
const newScrollHeight = getHeight(newItems)
const bottomOffset = -y.get() + scrollContainer
const bottomWillBeVisible = newScrollHeight < bottomOffset
const isScrollHeightLarger = newScrollHeight >= scrollContainer
if (bottomWillBeVisible && isScrollHeightLarger) {
controls.start({
y: -newScrollHeight + scrollContainer,
})
}
setItems(newItems)
}
return (
<Center>
<div
style={{
width: size,
height: size,
borderRadius: 30,
background: "transparent",
overflow: "hidden",
position: "relative",
transform: "translateZ(0)",
}}
>
<motion.div
style={{ y: y, height: totalScroll }}
drag="y"
dragDirectionLock
dragConstraints={{ top, bottom }}
animate={controls}
>
{items.map((value, index) => {
return (
<Item
total={items.length}
index={index}
onDelete={onDelete}
key={value}
/>
)
})}
</motion.div>
</div>
</Center>
)
}
function getHeight(items) {
const totalHeight = items.length * height
const totalPadding = (items.length - 1) * padding
const totalScroll = totalHeight + totalPadding
return totalScroll
}
function useConstraints(items) {
const [constraints, setConstraints] = React.useState({ top: 0, bottom: 0 })
React.useEffect(() => {
setConstraints({ top: size - getHeight(items), bottom: 0 })
}, [items])
return constraints
}
Some more details in the FM35SwipeToDelete()
component:
- The
onDelete()
function also checks for empty space at the bottom (which will occur when you delete the bottommost item) and will move the items down when needed (with auseAnimation()
). - A reference to this
onDelete()
function is passed down to every<Item>
.
This one doesn’t quite work in the current version of Framer Motion (4.2.1), so I left it in the CodeSandbox at Framer Motion 2.9.5. (Might be a bug. I asked the folks at Framer for more info.)
Code component
This one is a bit simpler because the Framer library contains such handy components as scroll and stack.
const initialItems = [0, 1, 2, 3, 4]
const height = 70
const padding = 10
const size = 150
function Item({ total, index, onDelete, ...rest }) {
const controls = useAnimation()
async function handleDragEnd(event, info) {
const offset = info.offset.x
const velocity = info.velocity.x
if (offset < -100 || velocity < -500) {
await controls.start({
x: "-100%",
transition: { duration: 0.2 },
})
onDelete(index)
} else {
controls.start({ x: 0, opacity: 1, transition: { duration: 0.5 } })
}
}
return (
<Frame
// Visual & layout
width={150}
height={height}
radius={20}
backgroundColor="transparent"
overflow="hidden"
// Layout animation
layout
transition={{
type: "spring",
stiffness: 600,
damping: 30,
}}
{...rest}
>
<Frame
// Visual & layout
width={size}
height={height}
radius={20}
backgroundColor="#fff"
// Dragging
drag="x"
dragDirectionLock
onDragEnd={handleDragEnd}
// Animation
animate={controls}
/>
</Frame>
)
}
export function CC35SwipeToDelete() {
const contentOffsetY = useMotionValue(0)
const contentHeight = useMotionValue(getHeight(initialItems))
const [items, setItems] = React.useState(initialItems)
function onDelete(index) {
const newItems = [...items]
newItems.splice(index, 1)
contentHeight.set(getHeight(newItems))
const bottomOffset = -contentOffsetY.get() + size
const bottomWillBeVisible = contentHeight.get() < bottomOffset
const isScrollHeightLarger = contentHeight.get() >= size
if (bottomWillBeVisible && isScrollHeightLarger) {
animate(contentOffsetY, -contentHeight.get() + size)
}
setItems(newItems)
}
return (
<Scroll
// Visual & layout
width={size}
height={size}
radius={30}
center
// Scrolling
contentOffsetY={contentOffsetY}
>
<Frame
width={150}
height={contentHeight}
backgroundColor="transparent"
>
{items.map((value, index) => {
return (
<Item
total={items.length}
index={index}
onDelete={onDelete}
key={value}
top={(height + padding) * index}
/>
)
})}
</Frame>
</Scroll>
)
}
function getHeight(items) {
const totalHeight = items.length * height
const totalPadding = (items.length - 1) * padding
const totalScroll = totalHeight + totalPadding
return totalScroll
}
Override
While it might be possible to create this interaction with overrides, it would be way more complicated. Not really worth the effort.
5 comments on “35. Swipe to Delete”
Leave a Reply
You must be logged in to post a comment.
Hi, author.
I want to make an effect when finger swipe right then it will be back to the previous page (like iOS edge hand gesture), but I have not found any API or tuition in Framer X Book. Can you offer me a solution or code?
Thanks a lot!
It would be difficult to do this with the current APIs. I suppose you’ll have to make the whole screen draggable and then track (with
onDragEnd()
) where the user released it. (Or do something by tracking a pan gesture.)This was a lot easier in Classic because it had an ‘edge swipe’ event and an API (the Flow Component) for navigating between screens. Here’s an example of that iOS swipe back gesture in Classic.
However, there’s a code API for Framer’s Link tool coming up. You can find more about it in this preview version of the docs. Maybe you can hack it together by tracking the swipe gesture with
onPan()
oronPanEnd()
and then calling that API’sgoBack()
function.Thanks for your replying. The goBack() function is what I need, but it to be provided in Framer API 1.1.7, and the Frame Web is not supported it, so I need to change the version of Framer?
No. You should be able to use it right now because the current Framer API is already version 1.2.2.
That Navigation API exists, it’s just not yet officially launched or explained in the docs. Here’s an example of how to use it made by Jordan at Framer.
Thanks a lot!