How to make ios like photo gallery using framer motion?

How to make ios like photo gallery using framer motion?

ยท

3 min read

Hello everyone, I'm Apoorv, currently working as a senior frontend engineer at Razorpay.

Lately, I've been exploring Framer and building quick prototypes. In this blog, I'll explain how to create an iOS-like photo gallery easily using Framer Motion.

A quick demo ๐Ÿช„

Let's begin ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป

To use images for the gallery, we would be using https://picsum.photos/id/ URL for generating random images.

As you may have noticed in the demo, when you click on an image, it expands and takes up the entire width and height of the container. When you click on the expanded image, it animates back to its original position. This animation behaviour is similar to that of the iOS photo gallery.

To implement this behaviour, we'll use a prop layoutId, which is provided by the Framer Motion component. It is useful when a component is removed from a React tree and added back to the tree, as it animates from the previous component's bounding box and its latest animated values.

If you didn't understand what that means, don't worry I'll explain in the later part of the blog.

Let's iterate over the images and render them.

      <motion.div
        className="flex flex-wrap gap-1 image-container relative"
        layout
      >
        {imageIds.map((i) => {
            return (
              <motion.div className="w-24 h-24" key={i}>
                <motion.img
                  layoutId={`img-${i}`}
                  src={`https://picsum.photos/id/${i}/800`}
                  onClick={() => openImage(i)}
                />
              </motion.div>
            );
          })}
      </motion.div>

When you click on an image, we want it to expand and take up the entire width and height. To achieve this, we can store the image ID in the state and then render it:

{selectedImage ? (
  <motion.img
    layoutId={`img-${selectedImage}`}
    src={`https://picsum.photos/id/${selectedImage}/800`}
    className="animate-open-image"
    onClick={() => setSelectedImage(null)}
  />
) : null}

Now combining these two code pieces

      <motion.div
        className="flex flex-wrap gap-1 image-container relative"
        layout
      >
        {imageIds.map((i) => {
          return (
            <motion.div className="w-24 h-24" key={i}>
              <motion.img
                layoutId={`img-${i}`}
                src={`https://picsum.photos/id/${i}/800`}
                onClick={() => openImage(i)}
              />
            </motion.div>
          );
        })}
        {selectedImage ? (
          <motion.img
            layoutId={`img-${selectedImage}`}
            src={`https://picsum.photos/id/${selectedImage}/800`}
            className="animate-open-image"
            onClick={() => setSelectedImage(null)}
          />
        ) : null}
      </motion.div>

In the iOS gallery, there's a spring-type animation when you click on an image. The image scales slightly and then takes up the entire width and height. To add that to our code, we can use transition={{ duration: 0.4, type: 'spring' }}.

We've also added a class animate-open-image to apply the following styles when the image component is rendered:

.animate-open-image {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

How does this work?

We utilized the layoutId prop while rendering all the images. When an image is clicked, it is given the same layoutId as it had when initially rendered.

layoutId={`img-${i}`} and layoutId={`img-${selectedImage}`}

When the expanded image is clicked, the selectedImage state variable becomes null, causing the expanded image to be removed from the React tree and animated back to its original position. The layoutId prop is crucial in animating the image back to its original position when it is removed from the tree. And that's how this animation is working.


That's all for this blog. I hope you found this blog helpful and informative. I'll be sharing more blogs around framer motion in the coming days. ๐ŸŽ‰

Feel free to play around with the embedded demo above. ๐Ÿ› 

If you have any questions, feel free to reach out to me on Twitter, GitHub

Did you find this article valuable?

Support Apoorv Taneja by becoming a sponsor. Any amount is appreciated!