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