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

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! (:

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!!!!