DEV Community

Cover image for Framer Motion e Intersection Observer: Uma Dupla Poderosa para Animações no React
Francielle Dellamora
Francielle Dellamora

Posted on • Edited on

15 3 2 1 2

Framer Motion e Intersection Observer: Uma Dupla Poderosa para Animações no React

Olá Pessoal,

Hoje vamos combinar o Framer Motion e a Intersection Observer API (IO API) para criar animações legais, portanto é importante compreender o que cada ferramenta oferece antes de começar.

A Intersection Observer API permite monitorar mudanças na interseção de um elemento marcado em relação ao seu pai ou à viewport de forma assíncrona. Já o framer-motion facilita a criação de animações, transições de layout e gestos de maneira declarativa, mantendo a semântica dos elementos HTML e SVG.

Os exemplos estão disponíveis no seguinte repositório.

Criando um observador

animations examples

Antes de começarmos a criar animações, precisamos criar o componente Section que utilizara o hook useInView para monitorarmos sua presença na tela.

Para identificarmos o elemento que será monitorado, aplicaremos a propriedade ref (que será o próprio section) e o parâmetro threshold que irá indicar, em porcentagem, a quantidade do elemento que precisa estar visível para que o estado inView seja atualizado.

Sempre que houver alterações no estado inView, o useEffect será acionado e chamará um callback no componente pai, permitindo que uma animação seja iniciada assim que o elemento entrar na tela.

export const Section = ({
  id,
  children,
  setIsInView,
  className,
}: Props): JSX.Element => {
  const { ref, inView } = useInView({
    threshold: 0.4,
  });

  useEffect(() => {
    if (setIsInView) {
      setIsInView(inView);
    }
  }, [inView, setIsInView]);

  return (
    <section
      className={`relative overflow-hidden ${className}`}
      ref={ref}
      id={id}
    >
      {children}
    </section>
  );
};

export default Section;
Enter fullscreen mode Exit fullscreen mode

Animação de títulos

Title animation example

No HTML, todo texto dentro de uma heading tag é considerado um elemento próprio na DOM, desse modo para a animação funcionar é necessário transformar cada caractere do texto em um elemento diferente.

O processo começa com a função split, que divide o título em palavras. Em seguida, a função map é usada para retornar cada palavra e repetimos a lógica para separá-la em caracteres únicos.
Para dar espaço entre as palavras, foi adicionado mr-2 no estilo (className). Além disso, a propriedade key é adicionada para garantir a identificação única de cada elemento e melhorar o desempenho da aplicação.

A fim de aproveitar a mágica do framer-motion, é necessário transformamos todas as tags de span em motions tags.
Dessa forma, o componente motion.span permite controlar a animação de cada caractere, definindo o estado inicial, animação, variações de animação e transições.

O uso da função useEffect também é necessário para simular o efeito triggerOnce do useInView e garantir que a animação ocorra apenas uma vez.

Finalmente, é preciso ajustar a propriedade transition para que cada caractere tenha o atraso adequado com base na sua posição.
O framer-motion permite que você controle a animação dos filhos com a propriedade staggerChildren na transição, então só precisamos adicionar ela e definir o tempo de delay.

No caso de títulos com mais de uma palavra, foi necessário usar o delayChildren e dividir o texto em palavras para atrasar a animação de cada uma delas. O processo envolve usar a função split, slice, join e length para determinar o tamanho total e multiplicá-lo pelo tempo especificado no staggerChildren acima.

const Title = ({
  title,
  triggerAnimation,
}: Props): JSX.Element => {
  const [triggered, setTriggered] = useState(false)

  useEffect(() => {
    setTriggered(curr => triggerAnimation || curr)
  },[triggerAnimation])


  const characterAnimation = {
    hidden: {
      opacity: 0,
    },
    visible: {
      opacity: 1,
    },
  };
  return (
      <div className="flex items-center">
        {title.split(" ").map((word, index) => {
          return (
            <motion.span
              className="mr-2"
              aria-hidden="true"
              key={`key-${word}-${index}`}
              initial="hidden"
              animate={triggered ? "visible" : "hidden"}
              transition={{
                staggerChildren: 0.1,
                delayChildren:
                  index === 0
                    ? 0
                    : title.split(" ").slice(0, index).join(" ").length * 0.1,
              }}
            >
              {word.split("").map((character, index) => {
                return (
                  <motion.span
                    className="text-2xl md:text-3xl text-gray  "
                    aria-hidden="true"
                    key={`key-${character}-${index}`}
                    variants={characterAnimation}
                  >
                    {character}
                  </motion.span>
                );
              })}
            </motion.span>
          );
        })}
      </div>
  );
};

export default Title;

Enter fullscreen mode Exit fullscreen mode

Animação de delay na opacity para textos

animations examples

O gif acima apresenta um componente que exibe duas colunas: uma coluna com parágrafos e outra com tópicos.

A coluna com parágrafos usa a função map para percorrer o array "paragraphs" e renderizar cada item como uma tag motion.p.
Cada tag motion.p tem a propriedade initial com valor de opacity: 0, o que significa que inicialmente a opacidade será 0%. A propriedade animate tem valor de opacity: 1, indicando que a animação deve mudar a opacidade para 100%.

A propriedade transition tem o valor delay: 1 + i * 0.2, o que significa que o tempo de atraso para cada tag será calculado pela soma de 1 mais o resultado da multiplicação de i por 0.2. O "i" é o valor do índice, começando em 0 e incrementando em 1 a cada iteração.

A segunda coluna exibe os tópicos e usa a função map para percorrer o array "topics" e renderizar cada item como uma tag "motion.li" e repetimos a mesma logica da animação dos parágrafos, com pequenas adaptações no valor de delay.

                             [...]
        <div className="md:flex gap-4">
          <div className="md:w-1/2">
          {paragraphs.map((paragraph, i) => {
            return (
              <motion.p
                initial={{opacity:0}}
                animate={{opacity: 1, transition: {delay: 1 + i * 0.2}}}
                className="text-justify " 
                key={`paragraph-${i}`}
              >
                {paragraph}
              </motion.p>
            )
          })}
          </div>
          <ul className="md:w-1/2 h-fit grid grid-cols-topics gap-4">
            {topics.map((topic, i) => {
              return (
                <motion.li
                  className={i === topics.length - 1 ? "lg:col-span-2": " "}
                  key={`topic-${i}`}
                  initial={{opacity: 0}}
                  animate={{ opacity: 1}}
                  transition={{                                                   
                    delay:                              
                    1.2 + 0.2 + i * 0.3,
                  }} 
                >                                                                                                                   
                <div className="flex items-start">
                <hr className="mr-2 mt-3 w-5 h-1 text-grayLight" />
                   {topic}
                   </div>
                 </motion.li>
              )
            })}
          </ul>
      </div>
                             [...]

Enter fullscreen mode Exit fullscreen mode

Animação para imagens durante o viewport on

animations examples

Para organizar o código e torná-lo mais limpo, usaremos a propriedade variants para controlar a posição, rotação e opacidade de quatro elementos diferentes. Utilizaremos a propriedade staggerChildren da transition para controlar a opacidade dos quatro elementos ao mesmo tempo.

Definiremos o posicionamento dos elementos usando os seguintes atributos:

  • y: que é a posição vertical dos elementos no eixo Y
  • x: que é a posição horizontal dos elementos no eixo X
  • rotate: que é a rotação dos elementos em graus

Para controlar a animação entre os estados, usaremos o objeto "transition" que inclui dois atributos:

  • type: "spring", indica que a animação usará uma transição "mola" (spring)

  • stiffness: 50, que indica a rigidez da mola. Quanto maior o número, mais rápida e suave será a animação.

const ExampleTwo: React.FC<ExampleTwoProps> = (): JSX.Element => {
  const [inView, setInView] = useState(false);
  const animations = {
    hidden: {
      opacity: 0
    },
    view: {
      opacity: 1,
    }
   }
  const firstGirl = {
    hidden: {
      y: 0, 
      x: -200, 
      rotate: "12deg"
    },
    view: {
      y:0, 
      x:-55, 
      rotate: "30deg", 
      transition: {
        type: "spring", 
        stiffness: 50
      }
    }
  }
                             [...]
  return (
                             [...]
      <motion.div
        className="flex flex-col"
        initial="hidden"
        animate={inView ? "view" : "hidden"}
        variants={animations}  
        transition={{staggerChildren: 0.5}}
      >
        <motion.img
          variants={firstGirl}
          src="/firstGirl.png"
          className=" absolute top-3 left-0  h-[21rem] lg:h-[25rem]"
        />
                             [...]
      </motion.div>
    </Section>
  );
};

export default ExampleTwo;

Enter fullscreen mode Exit fullscreen mode

Animação de switch para botão

animations examples

O componente ButtonExample é composto por um botão HTML e uma div, ambos estilizados com CSS. O botão tem uma cor que muda dependendo da propriedade active.

Quando clicado, ele executa a função onClick e a div é exibida apenas se a propriedade active for verdadeira.

As propriedades onClick, active e children permitem personalizar a funcionalidade e o conteúdo do botão.

const ButtonExample = ({
  onClick,
  active,
  children,
}: Props): JSX.Element => {
  return (
    <div className="relative w-full">
      <button
        className={` w-full flex relative font-Inter items-center text-xl  py-2 md:px-6 px-4 z-20 
        ${active ? "text-redLight" : "text-grayMedium"}  `}
        onClick={onClick}
      >
        {children}
      </button>
      {active && (
        <motion.div
          className=" rounded absolute top-0 bottom-0 left-0 right-0 bg-whiteBasic  z-10 flex justify-end"
          layoutId="buttonBg"
        />
      )}
    </div>
  );          
};

export default ButtonExample;

Enter fullscreen mode Exit fullscreen mode

Em conclusão, a união de Framer Motion e Intersection Observer no desenvolvimento de aplicações React é um passo importante para alcançar animações de alta qualidade.
A biblioteca Framer Motion oferece aos desenvolvedores a capacidade de criar animações complexas de forma simples, enquanto o Intersection Observer garante que as animações só sejam executadas quando os elementos estiverem na tela.
Juntos, eles permitem a criação de aplicações atraentes e interativas, proporcionando uma experiência fluida e envolvente aos usuários finais.

Ideias e comentários são bem-vindos e apreciados! (:

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (3)

Collapse
 
johnnymeneses profile image
Johnny Meneses

Muito bom e muito bem explicado.

Collapse
 
dellamora profile image
Francielle Dellamora

fico feliz que tenha gostado (:

Collapse
 
cauefidelis profile image
Caue Fidelis Sales

Incrivel!!!!

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay