Animation » Example Animations » 35. Swipe to delete

35. Swipe to delete

Quite a complicated example that combines layout animations, useAnimationControls(), and info provided by an onDragEnd() event.

Code component

A few details in the Item() component:

  • When the dragging stops (onDragEnd()), the handleDragEnd() function looks at the current offset or velocity.
  • When one of those is high enough:
    • an useAnimationControls() animation will slide the item offscreen,
    • and the onDelete() function in the parent component is called.
  • The items have a layout property (so that they animate to their new position automatically) and springy transition settings.
const initialItems = [0, 1, 2, 3, 4]
const height = 70
const padding = 10
const size = 150

function Item({ total, index, onDelete }) {
    const controls = useAnimationControls()

    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 default function CC_35_Swipe_to_delete(props) {
    const y = useMotionValue(0)

    const [items, setItems] = useState(initialItems)
    const { top, bottom } = useConstraints(items)
    const controls = useAnimationControls()
    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 (
        <div
            style={{
                width: 400,
                height: 400,
                ...props.style,
                display: "flex",
                placeItems: "center",
                placeContent: "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>
        </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 empty space at the bottom (occurs when you delete the bottommost item) and will move the items down when needed (with a useAnimationControls()).
  • 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 worth the effort.


5 comments on “35. Swipe to delete”

  • Vsplorer says:

    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!

    • Tes Mat says:

      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() or onPanEnd() and then calling that API’s goBack() function.

Leave a Reply