Reputation: 3
I try to build a simple Image Carousel with the help of framer-motion.
I want to use buttons and also drag to control the slides. It works just fine, but when i overshoot the slider on the last slide, it animates back to the start and mess up completely with the index logic.
Here is my code:
"use client";
import { useState } from "react";
import { AnimatePresence, motion, MotionConfig, PanInfo } from "framer-motion";
let images = [
"/images/carousel/1.jpeg",
"/images/carousel/2.jpeg",
"/images/carousel/3.jpeg",
"/images/carousel/4.jpeg",
"/images/carousel/5.jpeg",
];
export default function App() {
let [index, setIndex] = useState(0);
const dragEndHandler = (dragInfo: PanInfo) => {
const draggedDistance = dragInfo.offset.x;
const swipeThreshold = 50;
if (draggedDistance > swipeThreshold) {
console.log("swipe detection: ", "prev");
index > 0 && setIndex(index - 1);
} else if (draggedDistance < -swipeThreshold) {
console.log("swipe detection: ", "next");
index + 1 < images.length && setIndex(index + 1);
}
};
return (
<MotionConfig transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1] }}>
<div className="relative overflow-hidden">
<motion.div
animate={{ x: `-${index * 100}%` }}
className="flex"
drag="x"
dragElastic={1}
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={(_, dragInfo: PanInfo) => dragEndHandler(dragInfo)}
>
{images.map((image) => (
<div className="aspect-[4/5] w-full min-w-full">
<img
key={image}
src={image}
className="object-cover min-w-full pointer-events-none"
/>
</div>
))}
</motion.div>
<AnimatePresence initial={false}>
{index > 0 && (
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 0.7 }}
exit={{ opacity: 0, pointerEvents: "none" }}
whileHover={{ opacity: 1 }}
className="absolute left-2 top-1/2 -mt-4 flex h-8 w-8 items-center justify-center rounded-full bg-white"
onClick={() => setIndex(index - 1)}
>
<div className="h-6 w-6">Prev</div>
</motion.button>
)}
</AnimatePresence>
<AnimatePresence initial={false}>
{index + 1 < images.length && (
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 0.7 }}
exit={{ opacity: 0, pointerEvents: "none" }}
whileHover={{ opacity: 1 }}
className="absolute right-2 top-1/2 -mt-4 flex h-8 w-8 items-center justify-center rounded-full bg-white"
onClick={() => setIndex(index + 1)}
>
<div className="h-6 w-6">Next</div>
</motion.button>
)}
</AnimatePresence>
</div>
</MotionConfig>
);
}
I tried to use dragSnapToOrigin={false}
but this doesn't work with elastic and such. I think the issues occurs from the use of elastic and how i determine the drag direction. But i can't solve the problem here. Any help will be appreciated :)
Upvotes: 0
Views: 780
Reputation: 2057
The issue comes from the width of your motion.div
and it's dragConstraints
. I added some borders to your motion.div
and noticed it was only one slide wide. This was causing the dragConstraints
to not work how you would expect. When you get to the end of your carousel and drag right, the constraints kick in and resets the transform style to "none", so we see the carousel zoom back to the first slide, even though the index did not change. In my experience with framer motion, I've had the most success getting dragContraints
to behave how I think they should behave by using a ref instead of setting the top
, left
, etc.
These are the things that need to be updated to get the desired result:
dragConstraints
to use a ref that is attached to the parent container.(images.length * 100)%
wide.x
offset of the container to be one image's width (100 / images.length)%
times the index.min-w-full
class from the slide.Here is the relevant code snippet from this working example I created.
const constraintsRef = useRef(null);
...
{/* ↓ Parent Container */}
<div
// Added ref to Parent Container so we can use
// it's bounding box as dragConstraints
// https://www.framer.com/motion/gestures/###dragconstraints
ref={constraintsRef}
className="relative overflow-hidden"
>
{/* ↓ Draggable Container */}
<motion.div
style={{
// Set the width to be 100% of the Parent Container
// times the number of slides in the carousel. This
// helps make sure the dragConstraints work as expected.
width: `${images.length * 100}%`,
}}
animate={{
// Update the x offset to be one slides width. Since
// the width of the Draggable Container is
// images.length * 100, each slide's width is
// 100 / images.length.
x: `-${index * (100 / images.length)}%`,
}}
drag="x"
dragElastic={1}
dragConstraints={
// Set the drag constraints to the Parent Container's
// bounding box using a ref.
// https://www.framer.com/motion/gestures/###dragconstraints
constraintsRef
}
onDragEnd={(_, dragInfo: PanInfo) => dragEndHandler(dragInfo)}
className="flex"
>
{/* ↓ Slides map */}
{images.map((image, imageIndex) => (
<div
key={image + imageIndex}
// Removed min-w-full. It was causing the slide
// to be too large.
className="aspect-[4/5] w-full"
>
<img
src={image}
className="object-cover min-w-full pointer-events-none"
/>
</div>
))}
</motion.div>
...
</div>
Upvotes: 1