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
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;
Animação de títulos
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;
Animação de delay na opacity para textos
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> [...]
Animação para imagens durante o viewport on
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;
Animação de switch para botão
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;
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)
Muito bom e muito bem explicado.
fico feliz que tenha gostado (:
Incrivel!!!!