Search

32. Swipe to delete

32. 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.

⚡️ Some Examples – 32 – Swipe to delete
open in CodeSandbox

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(_, 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 function Example() {
    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
            style={{
                width: size,
                height: size,
                borderRadius: 30,
                backgroundColor: "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>
    );
}

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 Example() 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>.

Leave a Reply

Scroll to Top