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