Animações
Transições, keyframes e motion - interações suaves de UI com Tailwind CSS v4.
Busque em todas as páginas da documentação
Transições, keyframes e motion - interações suaves de UI com Tailwind CSS v4.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
// Transições
<button className="transition-colors duration-200 ease-in-out hover:bg-blue-600">
<div className="transition-all duration-300 hover:scale-105 hover:shadow-lg">
<div className="transition-opacity duration-150 opacity-0 data-[visible]:opacity-100">
// Animações integradas
<div className="animate-spin"> {/* rotação infinita */}
<div className="animate-ping"> {/* pulsa como um radar */}
<div className="animate-pulse"> {/* pulso suave de opacidade */}
<div className="animate-bounce"> {/* quicando */}
// Animação de keyframe personalizada
<div className="animate-fade-in"> {/* definida em @theme */}
// Movimento reduzido
<div className="motion-safe:animate-bounce motion-reduce:animate-none">/* globals.css - defina animações personalizadas */
@import "tailwindcss";
@theme {
--animate-fade-in: fade-in 0.3s ease-out;
--animate-slide-up: slide-up 0.4s ease-out;
--animate-scale-in: scale-in 0.2s ease-out;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes scale-in {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}Quando usar isso: Quando as interações da UI parecem abruptas - adicione transições para estados de hover, animações para elementos que entram e indicadores de carregamento.
"use client";
import { useState } from "react";
export function AnimatedNotificationList() {
const [notifications, setNotifications] = useState<
{ id: number; text: string; type: "info" | "success" | "error" }[]
>([]);
let nextId = 0;
function addNotification(type: "info" | "success" | "error") {
const id = ++nextId;
setNotifications((prev) => [
...prev,
{ id, text: `${type} notification #${id}`, type },
]);
setTimeout(() => {
setNotifications((prev) => prev.filter((n) => n.id !== id));
}, 3000);
}
const colors = {
info: "bg-blue-50 border-blue-200 text-blue-800",
success: "bg-green-50 border-green-200 text-green-800",
error: "bg-red-50 border-red-200 text-red-800",
};
return (
<div className="space-y-4">
<div className="flex gap-2">
<button
onClick={() => addNotification("info")}
className="rounded bg-blue-500 px-3 py-1.5 text-sm text-white transition-all duration-200 hover:bg-blue-600 active:scale-95"
>
Info
</button>
<button
onClick={() => addNotification("success")}
className="rounded bg-green-500 px-3 py-1.5 text-sm text-white transition-all duration-200 hover:bg-green-600 active:scale-95"
>
Success
</button>
<button
onClick={() => addNotification("error")}
className="rounded bg-red-500 px-3 py-1.5 text-sm text-white transition-all duration-200 hover:bg-red-600 active:scale-95"
>
Error
</button>
</div>
<div className="fixed top-4 right-4 z-50 flex w-80 flex-col gap-2">
{notifications.map((n) => (
<div
key={n.id}
className={`animate-slide-in-right rounded-lg border p-3 shadow-lg ${colors[n.type]}`}
>
<div className="flex items-center justify-between">
<p className="text-sm font-medium">{n.text}</p>
<button
onClick={() => setNotifications((prev) => prev.filter((x) => x.id !== n.id))}
className="ml-2 text-current opacity-50 transition-opacity hover:opacity-100"
>
X
</button>
</div>
{/* Barra de progresso */}
<div className="mt-2 h-0.5 animate-shrink rounded bg-current opacity-30" />
</div>
))}
</div>
</div>
);
}/* Adicione estes em globals.css */
@theme {
--animate-slide-in-right: slide-in-right 0.3s ease-out;
--animate-shrink: shrink 3s linear forwards;
}
@keyframes slide-in-right {
from { opacity: 0; transform: translateX(100%); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes shrink {
from { width: 100%; }
to { width: 0%; }
}O que isso demonstra:
animation-fill-mode: forwardsactive:scale-95 para feedback de clique de botãotransition-* controlam quais propriedades CSS transitam, duração e easing.animate-* aplicam o atalho animation - nome, duração, tempo, iteração.--animate-* em @theme, pareado com @keyframes.motion-safe: e motion-reduce: respeitam a media query prefers-reduced-motion.Grupos de propriedades de transição:
// Propriedades específicas (melhor performance)
<div className="transition-transform duration-300"> {/* apenas transform */}
<div className="transition-colors duration-200"> {/* apenas propriedades de cor */}
<div className="transition-opacity duration-150"> {/* apenas opacidade */}
<div className="transition-shadow duration-200"> {/* apenas box-shadow */}
<div className="transition-[transform,opacity] duration-300"> {/* conjunto personalizado */}Animações de entrada escalonadas:
{items.map((item, i) => (
<div
key={item.id}
className="animate-fade-in opacity-0"
style={{ animationDelay: `${i * 100}ms`, animationFillMode: "forwards" }}
>
{item.name}
</div>
))}Acordeão apenas com CSS:
<details className="group rounded border">
<summary className="cursor-pointer p-4 font-medium">
Clique para expandir
<span className="ml-2 inline-block transition-transform group-open:rotate-90">
▶
</span>
</summary>
<div className="overflow-hidden transition-all duration-300 group-open:max-h-96 max-h-0">
<div className="p-4 pt-0">Conteúdo oculto aqui</div>
</div>
</details>Esqueleto de carregamento:
function Skeleton({ className }: { className?: string }) {
return (
<div className={`animate-pulse rounded bg-gray-200 dark:bg-gray-700 ${className}`} />
);
}
// Uso
<div className="space-y-3">
<Skeleton className="h-6 w-3/4" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
</div>// Atraso da animação como prop
function AnimatedItem({
delay,
children,
}: {
delay: number;
children: React.ReactNode;
}) {
return (
<div
className="animate-fade-in opacity-0 [animation-fill-mode:forwards]"
style={{ animationDelay: `${delay}ms` }}
>
{children}
</div>
);
}
// Tipagem de evento de transição
function handleTransitionEnd(e: React.TransitionEvent<HTMLDivElement>) {
if (e.propertyName === "opacity") {
// Transição de opacidade concluída
}
}transition-all inclui propriedades de layout - Transicionar width, height ou padding aciona o recálculo de layout, causando travamentos. Correção: Use transition-transform e transition-opacity para animações suaves a 60fps.
Animação na montagem - Animações CSS executam na renderização inicial, o que pode causar um flash. Correção: Comece com opacity-0 e use animation-fill-mode: forwards para terminar com opacidade total.
prefers-reduced-motion é importante - Usuários com distúrbios vestibulares podem ser prejudicados pelo movimento. Correção: Envolva animações em motion-safe: ou forneça motion-reduce:animate-none.
animate-* repete em cada mudança de chave de re-renderização - Mudar a key do React remonta o elemento, repetindo a animação. Isso pode ser intencional ou um bug.
Camadas de GPU - Use will-change-transform com moderação. Muitas camadas de GPU desperdiçam memória. Correção: Adicione will-change apenas a elementos que estão ativamente animando.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Framer Motion | Você precisa de animações baseadas em gestos, layout e saída | Você só precisa de transições de hover simples |
API CSS @starting-style | Você precisa de animações de entrada CSS nativas (sem JS) | Você precisa de suporte amplo de navegador hoje |
| GSAP | Você precisa de animações complexas baseadas em linha do tempo | Transições CSS simples são suficientes |
| React Spring | Você quer animação baseada em física em React | Você prefere soluções nativas de CSS |
| API View Transitions | Você quer transições animadas de página para página | Você precisa de animações em nível de elemento |
transition-* - ativa em mudanças de estado (hover, focus, toggle de classe); controla quais propriedades transitam, duração e easing.animate-* - aplica uma animação @keyframes que executa na montagem ou quando acionada.@theme {
--animate-fade-in: fade-in 0.3s ease-out;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}Em seguida, use className="animate-fade-in".
transition-all inclui propriedades de layout como width e height, que acionam o recálculo de layout e causam travamentos. transition-transform e transition-opacity executam na GPU a 60fps.
{items.map((item, i) => (
<div
key={item.id}
className="animate-fade-in opacity-0"
style={{ animationDelay: `${i * 100}ms`, animationFillMode: "forwards" }}
>
{item.name}
</div>
))}Envolva animações em motion-safe: ou forneça um fallback com motion-reduce:animate-none. Estes mapeiam para a media query prefers-reduced-motion.
animate-spin - rotação infinitaanimate-ping - pulso estilo radaranimate-pulse - pulso suave de opacidadeanimate-bounce - efeito de quicarfunction Skeleton({ className }: { className?: string }) {
return (
<div className={`animate-pulse rounded bg-gray-200 dark:bg-gray-700 ${className}`} />
);
}Animações CSS executam a partir do estado atual do elemento. Se o elemento começar com opacity: 1, o primeiro frame é visível. Corrija começando com opacity-0 e usando animation-fill-mode: forwards (ou [animation-fill-mode:forwards]).
Não. Cada elemento will-change cria uma camada de GPU. Muitas camadas desperdiçam memória. Adicione will-change apenas a elementos que estão ativamente animando e remova-o depois.
function handleTransitionEnd(e: React.TransitionEvent<HTMLDivElement>) {
if (e.propertyName === "opacity") {
// transição de opacidade concluída
}
}function AnimatedItem({
delay,
children,
}: {
delay: number;
children: React.ReactNode;
}) {
return (
<div
className="animate-fade-in opacity-0 [animation-fill-mode:forwards]"
style={{ animationDelay: `${delay}ms` }}
>
{children}
</div>
);
}@theme para animaçõesRevisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥