Bring your web interfaces to life. Learn when to use CSS transitions versus keyframes and how to optimize your animations for peak performance.

Transform static interfaces into engaging experiences using native CSS, without the performance overhead of heavy JavaScript libraries. We have all looked at a static UI and wished for smooth, professional motion—but attempting to implement it often results in bloated, messy code that fails to perform. Master the techniques to build fluid, high-performance animations that keep your codebase clean and your user experience seamless.
Today, I will share CSS Animations - the art of bringing your interfaces to life in a performant and professional way. By the end of this article, you will understand when to use transitions vs animations, how specificity affects your animations, and how to create complex effects that impress.
CSS Animations are a native way to add movement and interactivity to your interfaces without relying on JavaScript. There are two main approaches:
Transitions: For smooth changes between two states
Animations (Keyframes): For complex and controlled sequences of changes
Before we dive into the implementation, let's understand the problem we are solving:
/* ❌ Sem animações - mudanças bruscas */.button { background-color: blue; transform: scale(1);}.button:hover { background-color: red; transform: scale(1.1); /* Mudança instantânea - experiência ruim */}/* ✅ Com animações - transições suaves */.button { background-color: blue; transform: scale(1); transition: all 0.3s ease;}.button:hover { background-color: red; transform: scale(1.1); /* Transição suave e profissional */}This transformation creates a more refined user experience, provides clear visual feedback, and adds personality to the interface.
Use Transitions when:
Changes between two states (hover, focus, active)
Simple and linear animations
Instant interaction feedback
Performance is critical
Use Animations (Keyframes) when:
Complex movement sequences
Animations that repeat
Precise timing control
Multiple properties changing at different moments
When NOT to use CSS animations:
Complex scroll-based animations (use Intersection Observer + CSS)
Manipulation of many elements simultaneously (consider Web Animations API)
Animations that depend on complex state logic
Let's build this step by step. I will show you how each piece works and why every decision matters.
First, we need to establish the correct structure. Here is how to do it properly:
Option 1: Transitions approach (Recommended for simple states)
/* Configuração base - sempre no elemento principal */.elemento { /* Propriedades iniciais */ background-color: #3498db; transform: translateX(0); opacity: 1; /* Transition - define COMO as mudanças acontecem */ transition: background-color 0.3s ease, transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease-out;}/* Estados de interação */.elemento:hover { background-color: #e74c3c; transform: translateX(20px);}.elemento:active { opacity: 0.8;}Option 2: Keyframes approach (For complex animations)
/* Definição da animação */@keyframes deslizarEEntrar { 0% { transform: translateX(-100%); opacity: 0; } 50% { opacity: 0.5; } 100% { transform: translateX(0); opacity: 1; }}/* Aplicação da animação */.elemento-animado { animation: deslizarEEntrar 0.6s ease-out forwards;}Why this setup works so well:
Transitions handle base element states; keyframes define animation steps separately.
Only non-layout properties like transform and opacity are animated, avoiding reflow and repaint.
Allows granular control with different timing functions per property, keeping code clean.
Key benefits of using transitions with keyframes
Separation of concerns: Transitions on the base element, states in pseudo-classes
Optimized performance: Animating only properties that do not cause reflow/repaint
Granular control: Different timing functions for different properties
Specificity is crucial for your animations to work correctly:
2.1: Basic Specificity Structure
/* Especificidade: 0,0,1,0 - Classe simples */.botao { background-color: blue; transition: background-color 0.3s ease;}/* Especificidade: 0,0,2,0 - Classe + pseudo-classe */.botao:hover { background-color: red;}/* Especificidade: 0,0,3,0 - Mais específico */.container .botao:hover { background-color: green; /* Esta regra vence */}2.2: Managing Conflicts with States
/* ⚠️ Problema comum - ordem inadequada */.botao:hover { background-color: red; /* Pode ser sobrescrito por regras menos específicas declaradas depois */}.botao.ativo { background-color: green; /* Esta regra pode não funcionar se hover estiver ativo */}/* ✅ Solução - especificidade adequada */.botao { background-color: blue; transition: background-color 0.3s ease;}.botao.ativo { background-color: green;}.botao:hover:not(.ativo) { background-color: red;}.botao.ativo:hover { background-color: darkgreen;}Important differences:
Calculated specificity: Classes (10) > Elements (1)
Declaration order: Last rule with the same specificity wins
Pseudo-classes increase specificity: :hover adds 10 points
2.3: Recommended Order in the Stylesheet
/* 1. Reset e variables CSS */:root { --primary-color: #3498db; --animation-duration: 0.3s; --easing: cubic-bezier(0.4, 0, 0.2, 1);}/* 2. Keyframes - sempre no topo */@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; }}@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; }}/* 3. Elementos base com animations/transitions */.componente { /* Propriedades estruturais primeiro */ display: flex; align-items: center; /* Propriedades visuais */ background-color: var(--primary-color); border-radius: 8px; /* Animations e transitions por último */ transition: all var(--animation-duration) var(--easing);}/* 4. Estados e modificadores em ordem de especificidade */.componente:hover { background-color: #2980b9;}.componente:active { transform: scale(0.98);}.componente.loading { animation: pulse 1.5s infinite;}/* Animações otimizadas para performance */.elemento-performatico { /* Usando transform e opacity - não causam reflow */ transform: translateZ(0); /* Força camada de composição */ will-change: transform, opacity; /* Avisa o browser sobre futuras mudanças */ transition: transform 0.3s var(--easing), opacity 0.3s var(--easing);}/* Gerenciamento de estados complexos */.elemento-performatico:hover { transform: translateY(-4px) scale(1.02);}.elemento-performatico:active { transform: translateY(-1px) scale(1.01); transition-duration: 0.1s; /* Feedback mais rápido para active */}Let's build something more realistic - a card system that demonstrates advanced use of animations:
Before implementing, let's understand what we are building:
/* ❌ Abordagem ingênua - problemas de performance */.card { /* Animando propriedades que causam reflow */ transition: width 0.3s, height 0.3s, box-shadow 0.3s;}.card:hover { width: 320px; /* Causa reflow */ height: 240px; /* Causa reflow */ box-shadow: 0 20px 40px rgba(0,0,0,0.3); /* Causa repaint */}/* ✅ Nossa abordagem otimizada - o que vamos construir */.card { /* Usando transform e filter - composited layers */ transition: transform 0.3s ease, filter 0.3s ease;}.card:hover { transform: scale(1.05) translateY(-8px); filter: drop-shadow(0 20px 40px rgba(0,0,0,0.3));}Phase 1: Base Card Structure
/* Keyframes para animações complexas */@keyframes cardAppear { 0% { opacity: 0; transform: translateY(30px) scale(0.95); } 50% { opacity: 0.5; transform: translateY(15px) scale(0.98); } 100% { opacity: 1; transform: translateY(0) scale(1); }}@keyframes shimmer { 0% { background-position: -1000px 0; } 100% { background-position: 1000px 0; }}/* Card base com performance otimizada */.card { /* Layout e estrutura */ position: relative; display: flex; flex-direction: column; width: 300px; height: 200px; border-radius: 12px; overflow: hidden; /* Camada de composição */ transform: translateZ(0); will-change: transform, filter; /* Transições otimizadas */ transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), filter 0.4s cubic-bezier(0.4, 0, 0.2, 1); /* Animação de entrada */ animation: cardAppear 0.6s ease-out backwards;}Analysis of this implementation:
translateZ(0): Forces a composition layer for better performance
will-change: Optimizes the browser for future changes
Custom cubic-bezier: Creates more natural movement
Phase 2: Advanced Interactive States
/* Estados com especificidade controlada */.card:hover { transform: translateY(-12px) scale(1.03); filter: drop-shadow(0 25px 50px rgba(0, 0, 0, 0.25)) brightness(1.05);}.card:active { transform: translateY(-6px) scale(1.01); transition-duration: 0.15s; /* Feedback rápido */}/* Estado de loading com animação contínua */.card.loading { pointer-events: none;}.card.loading::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient( 90deg, transparent, rgba(255, 255, 255, 0.4), transparent ); background-size: 1000px 100%; animation: shimmer 2s infinite linear; z-index: 1;}/* Estado de erro com animação de shake */@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); }}.card.error { animation: shake 0.5s ease-in-out; border: 2px solid #e74c3c;}Phase 3: Complete System with Variations
/* Variações de cards com diferentes animações */.card--featured { transform: scale(1.1); z-index: 2;}.card--featured:hover { transform: scale(1.13) translateY(-8px);}/* Cards com delay escalonado para listas */.card-list .card:nth-child(1) { animation-delay: 0ms; }.card-list .card:nth-child(2) { animation-delay: 100ms; }.card-list .card:nth-child(3) { animation-delay: 200ms; }.card-list .card:nth-child(4) { animation-delay: 300ms; }/* Micro-interações nos elementos internos */.card__image { transition: transform 0.6s ease; transform-origin: center;}.card:hover .card__image { transform: scale(1.1);}.card__title { transition: color 0.3s ease;}.card:hover .card__title { color: #3498db;}/* Botão interno com propagação controlada */.card__button { position: relative; overflow: hidden; transition: background-color 0.3s ease;}.card__button::before { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; background: rgba(255, 255, 255, 0.3); border-radius: 50%; transform: translate(-50%, -50%); transition: width 0.4s ease, height 0.4s ease;}.card__button:active::before { width: 300px; height: 300px;}Why this architecture is powerful:
Clear separation of concerns between states
Optimized performance with the use of composite layers
Controlled specificity avoiding conflicts
Micro-interactions that add personality
Now let's explore an advanced pattern that demonstrates professional-level usage.
/* ❌ Limitações da abordagem básica */.elemento1 { transition: transform 0.3s ease;}.elemento2 { transition: transform 0.3s ease; /* Duplicação */}.elemento3 { transition: transform 0.3s ease; /* Mais duplicação */}Why this becomes problematic:
Code duplication and inconsistencies
Difficult to maintain and update globally
Lack of standard between components
Stage 1: Foundation with Custom Properties
/* CSS Custom Properties para sistema de animações */:root { /* Durações padronizadas */ --duration-fast: 150ms; --duration-normal: 300ms; --duration-slow: 500ms; --duration-slower: 800ms; /* Easing functions padronizadas */ --ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94); --ease-out-cubic: cubic-bezier(0.215, 0.610, 0.355, 1); --ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1); --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275); /* Valores de transformação */ --scale-up: 1.05; --scale-down: 0.95; --translate-up: -8px; --shadow-low: 0 4px 8px rgba(0, 0, 0, 0.1); --shadow-high: 0 12px 24px rgba(0, 0, 0, 0.15);}/* Classes utilitárias reutilizáveis */.u-transition-fast { transition-duration: var(--duration-fast);}.u-transition-normal { transition-duration: var(--duration-normal);}.u-transition-smooth { transition: transform var(--duration-normal) var(--ease-out-cubic), filter var(--duration-normal) var(--ease-out-cubic);}.u-transition-spring { transition: transform var(--duration-slow) var(--ease-spring);}What this gives us:
Consistent and scalable system
Easy maintenance and global update
Semantic and intuitive naming
Stage 2: Reusable Interaction Patterns
/* Padrões de hover reutilizáveis */.pattern-lift { transition: transform var(--duration-normal) var(--ease-out-cubic), filter var(--duration-normal) var(--ease-out-cubic);}.pattern-lift:hover { transform: translateY(var(--translate-up)) scale(var(--scale-up)); filter: drop-shadow(var(--shadow-high));}.pattern-glow { position: relative; transition: filter var(--duration-normal) var(--ease-out-cubic);}.pattern-glow::before { content: ''; position: absolute; inset: -2px; background: linear-gradient(45deg, #ff006e, #8338ec, #3a86ff); border-radius: inherit; opacity: 0; filter: blur(10px); transition: opacity var(--duration-normal) var(--ease-out-cubic); z-index: -1;}.pattern-glow:hover::before { opacity: 0.7;}/* Padrão de loading reutilizável */.pattern-loading { position: relative; overflow: hidden;}.pattern-loading::after { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient( 90deg, transparent, rgba(255, 255, 255, 0.5), transparent ); animation: loading-sweep 1.5s infinite;}@keyframes loading-sweep { 0% { left: -100%; } 100% { left: 100%; }}Stage 3: Complete Implementation with Performance
/* Sistema completo de animações */.animation-system { /* Base para todos os elementos animados */ transform: translateZ(0); /* Força composite layer */ backface-visibility: hidden; /* Evita flickering */ perspective: 1000px; /* Para animações 3D suaves */}/* Estados focais com especificidade controlada */.animation-system:focus-visible { outline: 2px solid #3498db; outline-offset: 2px; transition: outline-offset var(--duration-fast) var(--ease-out-cubic);}/* Responsividade das animações */@media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }}/* Performance em dispositivos de baixa potência */@media (hover: none) { .pattern-lift:hover, .pattern-glow:hover { transform: none; filter: none; }}/* Container para animações escalonadas */.stagger-container { display: flex; flex-wrap: wrap; gap: 1rem;}.stagger-container > * { animation: slideInUp var(--duration-slow) var(--ease-out-cubic) backwards;}.stagger-container > *:nth-child(1) { animation-delay: 0ms; }.stagger-container > *:nth-child(2) { animation-delay: 100ms; }.stagger-container > *:nth-child(3) { animation-delay: 200ms; }.stagger-container > *:nth-child(4) { animation-delay: 300ms; }.stagger-container > *:nth-child(5) { animation-delay: 400ms; }@keyframes slideInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); }}Why this architecture is robust:
Scalable system based on design tokens
Optimized performance with composite layers
Accessibility considered with prefers-reduced-motion
Reusable patterns reduce code duplication
For TypeScript projects, here is how to make everything type-safe:
// types/animations.tsexport type AnimationDuration = 'fast' | 'normal' | 'slow' | 'slower';export type AnimationEasing = 'ease-out-quad' | 'ease-out-cubic' | 'ease-spring';export type AnimationPattern = 'lift' | 'glow' | 'loading';export interface AnimationConfig { duration: AnimationDuration; easing: AnimationEasing; delay?: number;}export interface AnimationSystemOptions { pattern: AnimationPattern; config: AnimationConfig; responsive?: boolean; reducedMotion?: boolean;}// utils/animationSystem.tsclass AnimationSystem { private static readonly DURATION_MAP: Record<AnimationDuration, string> = { fast: 'var(--duration-fast)', normal: 'var(--duration-normal)', slow: 'var(--duration-slow)', slower: 'var(--duration-slower)', }; static applyPattern( element: HTMLElement, options: AnimationSystemOptions ): void { const { pattern, config, responsive = true, reducedMotion = true } = options; // Aplicar classes baseadas nas opções element.classList.add(`pattern-${pattern}`); element.classList.add(`u-transition-${config.duration}`); if (responsive) { element.classList.add('animation-system'); } // Configurar delay se especificado if (config.delay) { element.style.animationDelay = `${config.delay}ms`; } }}What it solves: Creating rich and engaging feedback for user actions
How it works: Combination of multiple small animations with coordinated timing
/* Sistema de micro-interações para botões */.micro-button { position: relative; transform: translateZ(0); transition: all var(--duration-normal) var(--ease-out-cubic);}/* Sequência: hover -> focus -> active */.micro-button:hover { transform: translateY(-2px); filter: brightness(1.05);}.micro-button:focus-visible { transform: translateY(-2px) scale(1.02); box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);}.micro-button:active { transform: translateY(0) scale(0.98); transition-duration: var(--duration-fast);}/* Ripple effect interno */.micro-button::after { content: ''; position: absolute; inset: 0; background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%); border-radius: inherit; transform: scale(0); opacity: 0; transition: transform 0.6s ease, opacity 0.6s ease;}.micro-button:active::after { transform: scale(1); opacity: 1; transition-duration: 0s;}When to use: Main buttons, important CTAs, critical feedback elements
The problem: Conflicting animations between different application states
The solution: Mutually exclusive state system
/* Estados base com especificidade controlada */.state-element { transition: all var(--duration-normal) var(--ease-out-cubic);}/* Estados mutuamente exclusivos */.state-element[data-state="idle"] { opacity: 1; transform: scale(1);}.state-element[data-state="loading"] { opacity: 0.7; transform: scale(0.95); animation: pulse 2s infinite;}.state-element[data-state="success"] { opacity: 1; transform: scale(1.05); filter: hue-rotate(120deg); animation: successBounce 0.6s ease-out;}.state-element[data-state="error"] { opacity: 1; transform: scale(1); filter: hue-rotate(-30deg); animation: errorShake 0.5s ease-out;}@keyframes successBounce { 0%, 100% { transform: scale(1.05); } 50% { transform: scale(1.1); }}@keyframes errorShake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); }}Benefits: Clear states, no conflicts, easy debugging
Use case: Animations that only run when elements are visible
/* Animações preparadas mas não executadas */.animate-on-scroll { opacity: 0; transform: translateY(30px); transition: none; /* Inicialmente sem transition */}/* Classe aplicada via JavaScript quando visível */.animate-on-scroll.is-visible { opacity: 1; transform: translateY(0); transition: opacity var(--duration-slow) var(--ease-out-cubic), transform var(--duration-slow) var(--ease-out-cubic);}/* Variações para diferentes direções */.animate-on-scroll[data-direction="left"] { transform: translateX(-30px);}.animate-on-scroll[data-direction="right"] { transform: translateX(30px);}.animate-on-scroll[data-direction="up"] { transform: translateY(30px);}Context: Adapt animations based on screen size and device capability
/* Animações base para desktop */.responsive-animation { transition: transform var(--duration-normal) var(--ease-out-cubic);}.responsive-animation:hover { transform: translateY(-8px) scale(1.05);}/* Ajustes para tablets */@media (max-width: 1024px) { .responsive-animation:hover { transform: translateY(-4px) scale(1.02); }}/* Simplificação para mobile */@media (max-width: 768px) { .responsive-animation { transition-duration: var(--duration-fast); } .responsive-animation:hover { transform: scale(1.02); }}/* Dispositivos sem hover */@media (hover: none) { .responsive-animation:hover { transform: none; } .responsive-animation:active { transform: scale(0.98); }}The problem: Animations that cause constant reflow/repaint
/* ❌ Evite isto - propriedades que causam layout */.elemento-custoso { transition: width 0.3s, height 0.3s, padding 0.3s, margin 0.3s;}.elemento-custoso:hover { width: 200px; /* Causa reflow */ height: 150px; /* Causa reflow */ padding: 20px; /* Causa reflow */ margin: 10px; /* Causa reflow */}/* ✅ Faça isto - use transform e opacity */.elemento-otimizado { transition: transform 0.3s ease, opacity 0.3s ease;}.elemento-otimizado:hover { transform: scale(1.1); /* Composite layer */ opacity: 0.9; /* Composite layer */}Why it matters: Properties that cause layout can create janking at 60fps
Common error: Specificity battles that break animations
Why it happens: Lack of planning in the CSS structure
/* ❌ Problema - especificidade conflitante */.botao { background-color: blue; transition: background-color 0.3s ease;}.container .botao { background-color: green; /* Especificidade maior */}.botao:hover { background-color: red; /* Pode não funcionar */}/* ✅ Solução - especificidade planejada */.botao { background-color: blue; transition: background-color 0.3s ease;}.botao.variant-green { background-color: green;}.botao:hover,.botao.variant-green:hover { background-color: red;}Prevention: Use methodologies like BEM, keep specificity low and consistent
The trap: Ignoring users with motion sensitivities
/* ❌ Problema - animações forçadas */.elemento { animation: bounce 2s infinite;}@keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-20px); }}/* ✅ Solução - respeitar preferências do usuário */.elemento { animation: bounce 2s infinite;}@media (prefers-reduced-motion: reduce) { .elemento { animation: none; }}/* Ainda melhor - alternativa sutil */@media (prefers-reduced-motion: reduce) { .elemento { animation: fade 2s infinite; }}@keyframes fade { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; }}Warning signs: Animations that flash rapidly, constant movement without pause
Do not reach for CSS animations when:
Complex scroll-based animations: Use Intersection Observer + CSS or specialized libraries
Many elements simultaneously: Consider Web Animations API or GSAP for better performance
Complex animation logic: JavaScript may be more appropriate for conditional states
/* ❌ Exagero para cenários simples */.texto-simples { animation: rainbow 5s infinite linear;}@keyframes rainbow { 0% { color: red; } 16% { color: orange; } 33% { color: yellow; } 50% { color: green; } 66% { color: blue; } 83% { color: indigo; } 100% { color: violet; }}/* ✅ Solução simples é melhor */.texto-simples { color: #333; transition: color 0.3s ease;}.texto-simples:hover { color: #3498db;}Decision framework: Start simple, add complexity only when necessary
CSS is great for:
Native performance with built-in browser optimizations
Declarative syntax that is easy to read and maintain
Ideal for simple state changes like hover, focus, and active
Needed for conditional logic and decision-making
Better for complex synchronization of multiple animations
Suitable for data-driven and dynamic animation scenarios
Native performance: Out-of-the-box browser optimizations
Declarative: Easy to read and maintain
Simple states: Hover, focus, active work perfectly
Consider JavaScript when you need:
Conditional logic → Web Animations API: Refined programmatic control
Complex synchronization → GSAP: Advanced timeline and easing
Data-driven animations → D3.js + CSS: Dynamic visualizations
Feature | CSS | Web Animations API | GSAP |
|---|---|---|---|
Performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
Ease of use | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
Control | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Support | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
CSS Animations are a powerful tool that can transform static interfaces into engaging and professional experiences. They bring native performance, ease of maintenance, and seamless integration with the front-end development workflow.
The next time you encounter an interface that needs more life, remember the CSS Animations techniques. Your end user (and your team) will thank you for the more refined and professional experience.