DEV Community

Cover image for Creating Stunning 3D Instagram Story Swipes with React: A Step-by-Step Tutorial
Super
Super

Posted on

Creating Stunning 3D Instagram Story Swipes with React: A Step-by-Step Tutorial

Introduction

Have you ever used Instagram? If so, chances are you've come across Instagram Stories - a feature that has become increasingly popular in recent years. As someone who enjoys using Instagram, I found the animations used in Instagram Stories to be particularly amazing, and I decided to try and recreate them myself. And guess what? I succeeded! In this tutorial, I'm going to show you how to create a 3D Instagram story swipe using React, a popular JavaScript library for building user interfaces. With my help, you'll be able to add a creative and unique touch to your Instagram Stories that will make them stand out from the crowd. Let's get started!

Setup

We won't use any library other than React for this demo ( we can write this in plain JavaScript too )

Understand CSS 3D

Before going into the code. I'll explain some CSS attributes that are important with 3D animations:

  1. transform with transformZ and Y value: The transform property is used to apply transformations such as rotation, scaling, and translation to an element. When used in 3D, you can also use transformZ and transformY values to control the depth and height of the element respectively. For example, transform: translateZ(-50px) will move the element 50 pixels away from the viewer, creating a sense of depth.

  2. transform-style with preserve-3d value: The transform-style property is used to specify whether child elements should be flattened or preserve their 3D position. When set to preserve-3d, child elements will maintain their 3D positioning relative to the parent element. This is essential for creating complex 3D animations and designs.

  3. perspective: The perspective property is used to set the distance between the z=0 plane and the viewer in 3D space. This creates a sense of depth and perspective, making objects appear further away or closer to the viewer. For example, perspective: 1000px will set the viewer's perspective 1000 pixels away from the element, creating a greater sense of depth.

The Logic

States and constants

const [displayStories, setDisplayStories] = useState(data);
  const [imagePosition, setImagePosition] = useState(
    new Map(data.map((i) => [i.id, 0]))
  );
  const [cellSize, setCellSize] = useState(480);

  const [currentStory, setCurrentStory] = useState(data[0].id);
  const hold = useRef(0);
  const radiusRef = useRef(240 / Math.tan(Math.PI / 4));

  const carouselRef = useRef<HTMLDivElement | null>(null);
  const currentStoryRef = useRef(data[0].id);
  const [radius, setRadius] = useState(240 / Math.tan(Math.PI / 4));

  let isDown = false;
  let current = 0;
  let rotateYref = 0;
Enter fullscreen mode Exit fullscreen mode

This code block defines several state variables and references that are used to manage the 3D carousel effect.

const [displayStories, setDisplayStories] = useState(data); - This line defines a state variable called displayStories that holds an array of story objects. The useState hook is used to initialize the state with the data parameter passed in.

const [imagePosition, setImagePosition] = useState(new Map(data.map((i) => [i.id, 0]))); - This line defines a state variable called imagePosition that holds a Map object where each key is a story ID and the value is the index of the currently displayed image for that story. The useState hook is used to initialize the state with a new Map object that is created from the data parameter passed in.

const [cellSize, setCellSize] = useState(480); - This line defines a state variable called cellSize that holds the size of each carousel cell. The useState hook is used to initialize the state with a value of 480.

const [currentStory, setCurrentStory] = useState(data[0].id); - This line defines a state variable called currentStory that holds the ID of the currently displayed story. The useState hook is used to initialize the state with the ID of the first story in the data array.

const hold = useRef(0); - This line defines a reference called hold that is used to store a timeout ID.

const radiusRef = useRef(240 / Math.tan(Math.PI / 4)); - This line defines a reference called radiusRef that is used to store the radius of the carousel circle. The useRef hook is used to initialize the reference with a value calculated from the angle of view of the camera.

const carouselRef = useRef<HTMLDivElement | null>(null); - This line defines a reference called carouselRef that is used to reference the carousel div.

const currentStoryRef = useRef(data[0].id); - This line defines a reference called currentStoryRef that is used to reference the ID of the currently displayed story.

const [radius, setRadius] = useState(240 / Math.tan(Math.PI / 4)); - This line defines a state variable called radius that holds the radius of the carousel circle. The useState hook is used to initialize the state with a value calculated from the angle of view of the camera.

let isDown = false; - This line defines a boolean variable called isDown that is used to determine if the mouse is currently pressed down.

let current = 0; - This line defines a numeric variable called current that is used to keep track of the current rotation of the carousel.

let rotateYref = 0; - This line defines a numeric variable called rotateYref that is used to keep track of the current Y-rotation of the carousel.

The Events

There are some events that we need to listen when the component started render:

useEffect(() => {
    const carousel = document.getElementById("carousel") || ({} as Element);

    if (window.screen.width < 480) {
      setCellSize(window.screen.width);
      setRadius(window.screen.width / 2 / Math.tan(Math.PI / 4));
    }

    if (carousel) {
      carousel.addEventListener("mousedown", start as (e: Event) => void);
      carousel.addEventListener("touchstart", start as (e: Event) => void);

      carousel.addEventListener("mousemove", move as (e: Event) => void);
      carousel.addEventListener("touchmove", move as (e: Event) => void);

      carousel.addEventListener("mouseleave", end);
      carousel.addEventListener("mouseup", end);
      carousel.addEventListener("touchend", end);
    }
  }, []);
Enter fullscreen mode Exit fullscreen mode

The event listeners for "mousedown", "touchstart", "mousemove", "touchmove", "mouseleave", "mouseup", and "touchend" are added to the carousel element. These event listeners call the start, move, and end functions which are defined elsewhere in the code. These functions are used to handle the user's interactions with the carousel, such as clicking, dragging, and releasing.

The functions

There are 5 main functions that I want to cover in this demo:

  • prevStory : a function to go to the previous story
  • nextStory : a function to go to the next story
  • start : the function that would be triggered when we start click or touch the component
  • move : the function that would be triggered everytime we move the mouse or our finger
  • end : the function that would be trigger when we stop our animation

Let's go to the first 2 functions:

 const prevStory = (currentStoryIndex: number) => {
    setCurrentStory(data[currentStoryIndex - 1].id);
    rotateYref = hold.current + 90;
    if (carouselRef.current) {
      carouselRef.current.style.transform = `translateZ(-${
        radiusRef.current
      }px) rotateY(${hold.current + 90}deg)`;
    }

    hold.current = hold.current + 90;
  };

  const nextStory = (currentStoryIndex: number) => {
    setCurrentStory(data[currentStoryIndex + 1].id);
    rotateYref = hold.current - 90;
    if (carouselRef.current) {
      carouselRef.current.style.transform = `translateZ(-${
        radiusRef.current
      }px) rotateY(${hold.current - 90}deg)`;
    }
    hold.current = hold.current - 90;
  };
Enter fullscreen mode Exit fullscreen mode

These two functions, prevStory and nextStory, are used to navigate to the previous and next story in the carousel, respectively. The functions take in a currentStoryIndex parameter, which represents the index of the currently displayed story in the data array.

When prevStory is called, it sets the currentStory state variable to the ID of the previous story in the data array. It also updates the rotateYref variable to the current angle of rotation plus 90 degrees, and updates the carouselRef element's style to reflect the new angle of rotation. Finally, it updates the hold reference to reflect the new angle of rotation.

When nextStory is called, it sets the currentStory state variable to the ID of the next story in the data array. It also updates the rotateYref variable to the current angle of rotation minus 90 degrees, and updates the carouselRef element's style to reflect the new angle of rotation. Finally, it updates the hold reference to reflect the new angle of rotation.

Animation functions

 const end = () => {
    isDown = false;
    if (carouselRef.current) {
      carouselRef.current.style.transition = "transform 0.25s";
    }

    const currentStoryIndex = getCurrentIndex(currentStoryRef);
    if (rotateYref > hold.current && !isFirst(currentStoryIndex)) {
      prevStory(currentStoryIndex);
      return;
    }
    if (rotateYref < hold.current && !isLast(currentStoryIndex, data.length)) {
      nextStory(currentStoryIndex);
      return;
    }
    if (carouselRef.current) {
      carouselRef.current.style.transform = `translateZ(-${radiusRef.current}px) rotateY(${hold.current}deg)`;
    }
  };

  const start = (e: MouseEvent | TouchEvent) => {
    isDown = true;
    current = "pageX" in e ? e.pageX : e.touches[0].pageX;
  };

  const move = (e: MouseEvent | TouchEvent) => {
    if (!isDown || !carouselRef.current) return;
    e.preventDefault();
    carouselRef.current.style.transition = "none";
    const dist = "pageX" in e ? e.pageX : e.touches[0].pageX;
    const threshHold = Math.abs(dist - current);
    const wrap = 3.6666666;
    if (dist >= current) {
      rotateYref = hold.current + threshHold / wrap;
      carouselRef.current.style.transform = `translateZ(-${
        radiusRef.current
      }px) rotateY(${hold.current + threshHold / wrap}deg)`;
    } else {
      rotateYref = hold.current - threshHold / wrap;

      carouselRef.current.style.transform = `translateZ(-${
        radiusRef.current
      }px) rotateY(${hold.current - threshHold / wrap}deg)`;
    }
}
Enter fullscreen mode Exit fullscreen mode

These three functions are used to handle the user's interactions with the carousel element, such as clicking, dragging, and releasing.

end - This function is called when the user releases the mouse or touch on the carousel element. It sets the isDown variable to false to indicate that the mouse is no longer pressed. It also adds a transition effect to the carousel element, sets the currentStoryIndex to the current index of the displayed story, and checks if the carousel should rotate to the previous or next story, based on the rotateYref and hold.current variables. Finally, it updates the carouselRef element's style to reflect the new angle of rotation.

start - This function is called when the user clicks or touches the carousel element. It sets the isDownto true to indicate that the mouse is pressed, and sets the current to the current X position of the mouse or touch.

move - This function is called when the user moves the mouse or touch on the carousel element. It checks if the mouse is currently pressed down, and if so, it prevents the default behavior of the event. It also updates the rotateYref and carouselRef element's style to reflect the new angle of rotation based on the distance moved by the mouse or touch.

The User Interface

We will put all the code above into a React Hook, then we can use it in the component like this:

const {
    displayStories,
    imagePosition,
    cellSize,
    carouselRef,
    radius,
  } = useCarousel();
Enter fullscreen mode Exit fullscreen mode

The most important part right now is to put the radius into each element like this:

<div
  key={story.id}
  className="image-full absolute"
  style={{
    transform: `rotateY(${
     index * 90
    }deg) translateZ(${radius}px)`,
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    fontSize: 60,
  }}
>
Enter fullscreen mode Exit fullscreen mode

We will rotate the Y axis with index multiply with 90 degree. So we will have a cube.
With the wrapper, we need to set transform-style: preserve-3d; to it. And this will be the result:

Image description

Conclusion

To sum up, developing a 3D Instagram story carousel with React can significantly enhance the visual appeal of your Instagram Stories. By employing 3D CSS transformations and React, a revolving image carousel can be created with the appearance of depth and perspective. In this guide, we examined the essential features of 3D CSS, such as transform, transform-style, and perspective. Additionally, we discussed the React component code that constructs a 3D Instagram Stories carousel and explained the interplay of various functions and state variables in delivering an interactive, captivating user experience. With a touch of inventiveness and understanding of 3D CSS and React, you can develop your very own striking 3D Instagram story carousel that will astonish your audience and make your Stories more prominent.

Source Code: https://github.com/superdev163/3d-instagram
Live Demo: https://instagram-3d-poc.web.app/

Top comments (15)

Collapse
 
nishchit14 profile image
Nishchit

Very interactive tutorial. The demo looks like a professional work. Thanks for sharing.

Collapse
 
brainiacneit profile image
Super

Thanks. I put a lot of effort in the demo

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
emmanuelkatto profile image
Emmanuel Katto

Amazing, thanks for sharing!

Collapse
 
anogneva profile image
Anastasiia Ogneva

Thanks! Very interesting

Collapse
 
eduardovargasleffa profile image
Eduardo Vargas Leffa Abrantes

Thanks. Great content!!

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
solaris01 profile image
_solaris

Nice, I've always wondered how these were made.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
kwakyebrilliant profile image
a_moah__

Great content

Collapse
 
brainiacneit profile image
Super

Thanks !

Collapse
 
ashutoshmishra profile image
Ashutosh Mishra

Amazing! Would love to see other interesting projects from you!

Collapse
 
paulsalamone profile image
Paul Salamone

very cool, one constructive idea: show us the demo earlier in the article :)

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
Sloan, the sloth mascot
Comment deleted