Some Examples » 35. Swipe to delete
35. Swipe to delete
Quite a complicated example that combines layout
animations, useAnimate()
, animating a Motion value with animate()
, and info provided by an onDragEnd()
event.
Code component
A few details in the Item()
component:
- When the dragging stops (
onDragEnd()
event), thehandleDragEnd()
function looks at the currentoffset
andvelocity
(values provided by the event). - When one of those is high enough:
- A
useAnimate()
animation will slide the item offscreen, - and after a small
setTimeout()
, theonDelete()
function in the parent component is called.
- A
- The items have a
layout
property so that they animate to their new position automatically, with springytransition
settings.
const initialItems = [0, 1, 2, 3, 4]
const height = 70
const padding = 10
const size = 150
function Item({ total, index, onDelete }) {
const [scope, animate] = useAnimate()
function handleDragEnd(event, info) {
const offset = info.offset.x
const velocity = info.velocity.x
if (offset < -100 || velocity < -500) {
animate(scope.current, { x: "-100%" }, { duration: 0.2 })
setTimeout(() => onDelete(index), 200)
} else {
animate(scope.current, { x: 0, opacity: 1 }, { 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}
ref={scope}
/>
</motion.div>
)
}
export default function CC_35_Swipe_to_delete(props) {
const y = useMotionValue(0)
const [items, setItems] = useState(initialItems)
const { top, bottom } = useConstraints(items)
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) {
animate(y, -newScrollHeight + scrollContainer)
}
setItems(newItems)
}
return (
<div>
<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 }}
>
{items.map((value, index) => {
return (
<Item
total={items.length}
index={index}
onDelete={onDelete}
key={value}
/>
)
})}
</motion.div>
</div>
</div>
)
}
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] = useState({ top: 0, bottom: 0 })
useEffect(() => {
setConstraints({ top: size - getHeight(items), bottom: 0 })
}, [items])
return constraints
}
Some more details in the main CC_35_Swipe_to_delete()
component:
- The
onDelete()
function also checks for space at the bottom (occurs when you delete the bottommost item) and will adjust the position (they
Motion value) of the draggable<motion.div>
when needed, with ananimate()
animation. - A reference to this
onDelete()
function is passed down to every<Item>
.
Code override
While it might be possible to create this interaction with overrides, it would be way more complicated. Not really the effort.