Framer X » Animation » Example animations » 35. Swipe To Delete

35. Swipe To Delete

Open Framer Motion version in CodeSandbox

Framer Motion

Quite a complicated example. A lot is going on here.

A few details in the Item component:

  • When the dragging stopped (onDragEnd()) the handleDragEnd() function looks at the current offset or velocity,
  • …when one of them is high enough the useAnimation() animation will slide the item offscreen,
  • …and then the onDelete() on the parent component gets called.
  • There’s a springy positionTransition, so when the parent component moves the items, this is how they will animate.
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" }}
            positionTransition={{
                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 an useAnimation()).
  • A reference to this onDelete() function is passed down to every item.

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 }) {
    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"
            // Position transition
            positionTransition={{
                type: "spring",
                stiffness: 600,
                damping: 30,
            }}
        >
            <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)

    const controls = useAnimation()

    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) {
            controls.start({
                y: -contentHeight.get() + size,
            })
        }

        setItems(newItems)
    }

    return (
        <Scroll
            // Visual & layout
            width={size}
            height={size}
            radius={30}
            center
            // Scrolling
            contentOffsetY={contentOffsetY}
            contentHeight={contentHeight.get()}
            // Scroll animation
            scrollAnimate={controls}
        >
            <Stack width={150} padding={0} gap={padding}>
                {items.map((value, index) => {
                    return (
                        <Item
                            total={items.length}
                            index={index}
                            onDelete={onDelete}
                            key={value}
                        />
                    )
                })}
            </Stack>
        </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.


Leave a Reply