matt
matt

Reputation: 320

How to add a delay to an animation if it's in the viewport initially, otherwise have a different delay?

I'm trying to build a website using React and framer motion and I'm running into a challenge.

If an element is in the viewport when the page initially loads, I want there to be a delay on the animation. But if the element isn't in the viewport, and the user has to scroll to see it, I want there to be a different delay value.

So for example, on my website I have a header Lorem ipsum and some text that I render first. Then I have a Projects header that renders second. And lastly, a My Project block that renders third.

animation 1

This looks great if the window height is big enough so that everything loads in at once.

But if the window height is smaller, like in the GIF below, the My Project block takes too long to load in.

animation 2

I'd like to have it so that it loads in faster like this:

animation 3

I tried searching online to see if someone has had this problem before, but I couldn't find any posts online about this.

Any ideas on how to solve this?

My code:

// page.tsx

<main>
  <div>
    <motion.h1
      initial={{ opacity: 0, y: 10 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ ease: 'linear', duration: 0.5, delay: 0.15 }}
    >
      Lorem ipsum
    </motion.h1>
    <motion.div
      initial={{ opacity: 0, y: 10 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ ease: 'linear', duration: 0.5, delay: 0.15 }}
    >
      <h1>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
        tempor incididunt ut labore
      </h1>
      <h1>
        Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
      </h1>
    </motion.div>
  </div>
  <motion.h2
    initial={{ opacity: 0, y: 10 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ ease: 'linear', duration: 0.5, delay: 0.8 }}
  >
    Projects
  </motion.h2>
  <div>
    <Card
      title={cards[0].title}
      description={cards[0].description}
      index={0}
    />
  </div>
</main>
// Card.tsx

type CardProps = {
  title: string;
  description: string;
  index: number;
};

export default function Card({
  title,
  image,
  altText,
  slug,
  index,
  description,
}: CardProps) {
  delay =  1.05 + index * 0.2; // default delay
  // delay = 0.25; // delay I want if it's not in the viewport on first page load

  return (
    <motion.div
      initial={{ opacity: 0, y: 10 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{
        ease: 'linear',
        duration: 0.5,
        delay: delay,
      }}
    >
      <p>{title}</p>
      <p>{description}</p>
      <a>
        Learn more
        <svg>...</svg>
      </a>
    </motion.div>
  );
}

Upvotes: 0

Views: 49

Answers (1)

matt
matt

Reputation: 320

I solved it! It took me a while to think of a solution because useInView and IntersectionObserver will tell you when something enters the viewport, but they don't tell you if an element is in the view port on first page load.

The solution is surprisingly simple and concise:

const ref = useRef<HTMLDivElement>(null);
const [delay, setDelay] = useState(1.05 + index * 0.2);

useEffect(() => {
  if (ref.current) {
    if (ref.current.getBoundingClientRect().top > window.innerHeight) {
      setDelay(0.25);
    }
  }
}, []);

return (
  <motion.div
    ref={ref}
    initial={{ opacity: 0, y: 10 }}
    whileInView={{ opacity: 1, y: 0 }}
    viewport={{ once: true }}
    transition={{
      ease: 'linear',
      duration: 0.5,
      delay: delay,
    }}
  >
    // ...
)

Upvotes: 0

Related Questions