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), the handleDragEnd() function looks at the current offset and velocity (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(), the onDelete() function in the parent component is called.
  • The items have a layout property so that they animate to their new position automatically, with 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 [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 (the y Motion value) of the draggable <motion.div> when needed, with an animate() 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.


Join the Mighty Guides mailing list    ( ± 6 emails/year )

GDPR

We use Mailchimp as our marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing per their Privacy Policy and Terms.



Leave a Reply