user18943198
user18943198

Reputation:

Overlapping images animation effect while scrolling

I got inspired to create this animation effect. What I want to achieve is that the overlapped images get a little bigger when scrolling down and then again smaller when I scroll back.

For the scrolling part I know I need to use Intersection Observer API. I think I managed it to do right but I cant get it to work. I use React Typescript with inline styling.

The original animation - Three overlaping images - getting bigger on scroll down:

Codepen

My React Code - OverlappingImages.tsx :



import React from 'react';

const styles = {
    container: {
        position: 'relative',
        height: '400px',
        margin: '0 50px',

        div: {
            width: '380px',
            border: '1px solid #000',
            overflow: 'hidden',
            lineHeight: 0,
            transition: 'transform .4s ease-in-out',

            img: {
                width: '100%',
                fontSize: 0,
            },
        },

        img1: {
            left: '5%',
            top: 0,
            position: 'absolute',
            transform: 'rotate(-4deg) translateY(20%)',
            transitionDelay: '0s',
        },

        img2: {
            left: '50%',
            top: 0,
            position: 'absolute',
            transform: 'translate(-50%, 0)',
            transitionDelay: '.1s',
            zIndex: 1,
        },

        img3: {
            right: '5%',
            top: 0,
            position: 'absolute',
            transform: 'rotate(4deg) translateY(20%)',
            transitionDelay: '.2s',
        },

        ' &.active': {
            img1: {
                transform: 'rotate(-6deg) translateY(50%) scale(1.9)',
            },

            img2: {
                transform: 'translate(-50%, -2%) scale(1.2)',
            },

            img3: {
                transform: 'rotate(6deg) translateY(24%) scale(1.2)',
            },
        },
    },

    body: {
        fontFamily: 'sans-serif',
        fontSize: '48px',
        fontWeight: 'bold',
        letterSpacing: '1px',
        margin: 0,
    },

    section: {
        textAlign: 'center',
        padding: '500px 0',

        '&:nth-child(odd)': {
            background: '#eee',
        },
    },
};

function OverlappingImages() {
    const wrapper = document.querySelector('.container');
    const className = 'active';

    const observer = new IntersectionObserver(
        (entries) => {
            entries.forEach((entry) => {
                if (entry.isIntersecting) {
                    wrapper.classList.add(className);
                    return;
                }

                wrapper.classList.remove(className);
            });
        },
        {
            threshold: 1,
        }
    );

    observer.observe(wrapper);

    return (
        <>
            <section>
                <p>(scroll down!)</p>
            </section>
            <section>
                <div style={styles.container}>
                    <div style={styles.container.img1}>
                        <img src="https://via.placeholder.com/350x250" alt="img1" />
                    </div>
                    <div style={styles.container.img2}>
                        <img src="https://via.placeholder.com/350x250" alt="img2" />
                    </div>
                    <div style={styles.container.img3}>
                        <img src="https://via.placeholder.com/350x250" alt="img3" />
                    </div>
                </div>
            </section>
            <section>
                <p>(scroll up!)</p>
            </section>
        </>
    );
}

export { OverlappingImages };




Upvotes: 2

Views: 819

Answers (2)

Anton
Anton

Reputation: 8518

The styled-components approach:

  1. I created dummy data for the loop.
  2. Created simple components for section, figure and img. I used figure as a wrapper.
  3. Replaced all necessary style from img to figure and changed styled logic from position: absolute to grid solution. It will allow us to keep the images in the center of the screen if screen size is large and make it flexible for the small screens.
  4. The PictureWrapper (figure) can pass 2 props, position and state.

resize

scroll

OverlappingImages.tsx

import { useRef, useEffect, useState, useMemo } from "react";
import styled, { css } from "styled-components";
import data from "./data";
export type TypePosition = "left" | "center" | "right";

interface IProps {
  position: TypePosition;
  active: boolean;
}

const Image = styled.img`
  width: 100%;
  height: auto;
`;

// Left image wrapper style with active, inactive state
const left = (active: boolean) => css`
  ${!active && css`transform: rotate(-4deg) translateX(calc(-1 * clamp(25%, 20vw, 75%)));`}
  ${active && css`transform: rotate(-6deg) translateX(calc(-1 * clamp(25%, 20vw, 75%))) scale(1.2);`}
  transition-delay: 0s;
  z-index: 1;
`;

// Center image wrapper style with active, inactive state
const center = (active: boolean) => css`
  ${active && css`transform: scale(1.2);`}
  transition-delay: 0.1s;
  z-index: 2;
`;

// Right image wrapper style with active, inactive state
const right = (active: boolean) => css`
  ${!active && css`transform: rotate(4deg) translateX(clamp(25%, 20vw, 75%));`}
  ${active && css`transform: rotate(6deg) translateX(clamp(25%, 20vw, 75%)) scale(1.2);`}
  transition-delay: 0.2s;
  z-index: 1;
`;

// Image wrapper component with 2 props:
// position: left | center | right
// active:  true / false
const PictureWrapper = styled.figure<IProps>`
  grid-column: 1;
  grid-row: 1;
  width: clamp(200px, 40vw, 380px);
  display: flex;
  border: 1px solid #000;
  transition: transform 0.4s ease-in-out;
  ${({ position, active }) => position === "left" && left(active)}
  ${({ position, active }) => position === "center" && center(active)}
  ${({ position, active }) => position === "right" && right(active)}
`;

const Container = styled.section`
  display: grid;
  place-content: center;
  position: relative;
  margin: 0 50px;
`;

export const OverlappingImages = () => {
  const [active, setActive] = useState(false);
  const ref = useRef<HTMLElement>(null);

  const callback = (entries: IntersectionObserverEntry[]) => {
    const [entry] = entries;
    if (entry.isIntersecting) {
      setActive(entry.isIntersecting);
      return;
    }
    setActive(false);
  };

  const options = useMemo(() => ({
    root: null,
    rootMargin: "0px",
    threshold: 0.75
  }), []);

  useEffect(() => {
    const container = ref.current;
    // Observer with external callback function and options
    const observer = new IntersectionObserver(callback, options);
    if (container) observer.observe(container);

    //cleanup when a component unmounted
    return () => {
      if (container) observer.unobserve(container);
    };
  }, [ref, options]);

  const images = data.map((img) => {
    return (
      <PictureWrapper key={img.id} position={img.position} active={active}>
        <Image src={img.image} />
      </PictureWrapper>
    );
  });
  return <Container ref={ref}>{images}</Container>;
};

data.ts

import { TypePosition } from "./OverlappingImages";
interface IData {
  id: string;
  image: string;
  position: TypePosition;
}
export const data: IData[] = [
  {
    id: "d4a54w5s1d2sd24",
    image: "https://via.placeholder.com/350x250",
    position: "left"
  },
  {
    id: "ad4e5qe4545d7ew4",
    image: "https://via.placeholder.com/350x250",
    position: "center"
  },
  {
    id: "das54w5e1sa2dw5e5",
    image: "https://via.placeholder.com/350x250",
    position: "right"
  }
];
export default data;

App.tsx

import "./styles.css";
import { OverlappingImages } from "./OverlappingImages";

export default function App() {
  return (
    <div className="App">
      <section>
        <p>(scroll down!)</p>
      </section>
      <OverlappingImages />
      <section>
        <p>(scroll up!)</p>
      </section>
    </div>
  );
}

sections style

section {
  display: grid;
  place-content: center;
  min-height: 100vh;
  text-align: center;
}
section:nth-child(odd) {
  background: #eee;
}

Edit dazziling-code

Upvotes: 0

Alireza
Alireza

Reputation: 643

Here's the result:

solution result

  • You need to wrap your code above reutrn(), into the window.onload because if you run it in the way your currently doing it, document.querySelector('.container') is going to return nothing but null or undefined

  • Your container has no class or id and your trying to access it with document.querySelector('.container') again you'll get null

Make sure you assign an id or a class to it

Style.css

#container * {
  transition: all .5s ease;
}
.active  div:nth-child(1) {
  transform: rotate(-4deg) translateY(20%) scale(1.1) !important;
}
.active  div:nth-child(2) {
  transform: translate(-50%, 0%) scale(1.1) !important;
}
.active  div:nth-child(3) {
  transform: rotate(4deg) translateY(20%) scale(1.1) !important;
}

OverlappingImages.tsx

const styles = {
  container: {
    position: "relative",
    height: "400px",
    margin: "0 50px",
    padding: "30px",
    transition: "all .5s ease",

    img1: {
      left: "5%",
      top: 0,
      position: "absolute",
      transform: "rotate(-4deg) translateY(20%)",
      transitionDelay: "0s",
    },

    img2: {
      left: "50%",
      top: 0,
      position: "absolute",
      transform: "translate(-50%, 0)",
      transitionDelay: ".1s",
      zIndex: 1,
    },

    img3: {
      right: "5%",
      top: 0,
      position: "absolute",
      transform: "rotate(4deg) translateY(20%)",
      transitionDelay: ".2s",
    },
  },

  whiteSpace: {
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    height: "100vh",
  },
};

function OverlappingImages() {
  window.onload = function () {
    const wrapper = document.querySelector("#container");
    const className = "active";

    let preY = 0, preR = 0;
    const observer = new IntersectionObserver(
      entries => {
        entries.forEach(e => {
          const currentY = e.boundingClientRect.y;
          const currentR = e.intersectionRatio;

          if (currentY < preY || e.isIntersecting) {
            wrapper?.classList.add(className);
          } else if (currentY > preY && currentR < preR) {
            wrapper?.classList.remove(className);
          }

          preY = currentY;
          preR = currentR;
        });
      },
      { threshold: 0.8 }
    );
    observer.observe(wrapper);
  };

  return (
    <>
      <section>
        <div style={styles.whiteSpace}>
          <p>(scroll down!)</p>
        </div>
      </section>
      <section>
        <div style={styles.container} id="container">
          <div style={styles.container.img1}>
            <img src="https://via.placeholder.com/350x250" alt="img1" />
          </div>
          <div style={styles.container.img2}>
            <img src="https://via.placeholder.com/350x250" alt="img2" />
          </div>
          <div style={styles.container.img3}>
            <img src="https://via.placeholder.com/350x250" alt="img3" />
          </div>
        </div>
      </section>
      <section>
        <div style={styles.whiteSpace}>
          <p>(scroll up!)</p>
        </div>
      </section>
    </>
  );
}

export default OverlappingImages;

Second approach(using ref)


Style.css

.active div:nth-child(1) {
  transform: rotate(-4deg) translateY(20%) scale(1.1) !important;
}
.active div:nth-child(2) {
  transform: translate(-50%, 0%) scale(1.1) !important;
}
.active div:nth-child(3) {
  transform: rotate(4deg) translateY(20%) scale(1.1) !important;
}

OverlappingImages.tsx

import {useRef, useEffect} from 'react';
const styles = {
  container: {
    position: "relative",
    height: "400px",
    margin: "0 50px",
    padding: "30px",

    img1: {
      left: "5%",
      top: 0,
      position: "absolute",
      transform: "rotate(-4deg) translateY(20%)",
      transition: "all .5s ease",
    },

    img2: {
      left: "50%",
      top: 0,
      position: "absolute",
      transform: "translate(-50%, 0)",
      transition: "all .5s ease .1s",
      zIndex: 1,
    },

    img3: {
      right: "5%",
      top: 0,
      position: "absolute",
      transform: "rotate(4deg) translateY(20%)",
      transition: "all .5s ease .2s",
    },
  },

  whiteSpace: {
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    height: "100vh",
  },
};

function OverlappingImages() {
  const ref = useRef(null);

  useEffect(()=>{
    const wrapper = ref.current;
    const className = "active";

    let preY = 0, preR = 0;
    const observer = new IntersectionObserver(
      entries => {
        entries.forEach(e => {
          const currentY = e.boundingClientRect.y;
          const currentR = e.intersectionRatio;

          if (currentY < preY || e.isIntersecting) {
            wrapper?.classList.add(className);
          } else if (currentY > preY && currentR < preR) {
            wrapper?.classList.remove(className);
          }

          preY = currentY;
          preR = currentR;
        });
      },
      { threshold: 0.8 }
    );
    observer.observe(wrapper);
  },[])
    

  return (
    <>
      <section>
        <div style={styles.whiteSpace}>
          <p>(scroll down!)</p>
        </div>
      </section>
      <section>
        <div ref={ref} style={styles.container}>
          <div style={styles.container.img1}>
            <img src="https://via.placeholder.com/350x250" alt="img1" />
          </div>
          <div style={styles.container.img2}>
            <img src="https://via.placeholder.com/350x250" alt="img2" />
          </div>
          <div style={styles.container.img3}>
            <img src="https://via.placeholder.com/350x250" alt="img3" />
          </div>
        </div>
      </section>
      <section>
        <div style={styles.whiteSpace}>
          <p>(scroll up!)</p>
        </div>
      </section>
    </>
  );
}

export default OverlappingImages;

Upvotes: 1

Related Questions