MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

CSS Animations vs Transitions: A Practical Guide

Master CSS animations and transitions with performance tips and hardware acceleration.

CSSAnimationsPerformanceFrontend

By MinhVo

Introduction

Motion design is a critical part of modern web interfaces. Subtle animations guide users through interactions, provide feedback on actions, and create a sense of continuity between states. CSS provides two powerful mechanisms for adding motion: transitions and animations. While they might seem interchangeable, they serve fundamentally different purposes and understanding when to use each one is essential for building polished, performant interfaces.

CSS transitions handle simple state changes — a button that changes color on hover, a modal that fades in when opened, or a sidebar that slides into view. They activate automatically when a CSS property changes value. CSS animations, on the other hand, handle complex, multi-step motion sequences that need precise timing control, looping behavior, or animations that run on page load without any user interaction.

Motion design principles in CSS

This guide explores the technical differences between CSS transitions and animations, walks through practical implementation patterns for each, and covers the performance characteristics you need to understand to ship smooth 60fps motion on the web.

Understanding Animations and Transitions: Core Concepts

CSS Transitions

A CSS transition defines how a property should animate when its value changes. You specify four things: which properties to transition, how long the transition takes, what easing function to use, and optionally a delay before the transition starts.

.button {
  background-color: #0066ff;
  transition-property: background-color;
  transition-duration: 0.2s;
  transition-timing-function: ease-out;
  transition-delay: 0s;
}
 
.button:hover {
  background-color: #0052cc;
}

The shorthand syntax combines all four properties:

.button {
  transition: background-color 0.2s ease-out;
}

Transitions are reactive — they only fire when a property value changes due to a state change (hover, focus, class toggle, JavaScript style change). They cannot run on page load or loop indefinitely.

CSS Animations

CSS animations use @keyframes to define multi-step sequences with precise control over each step. Unlike transitions, animations can run on page load, loop infinitely, alternate direction, and combine multiple properties with different timing at each keyframe.

@keyframes slide-in {
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}
 
.sidebar {
  animation: slide-in 0.3s ease-out forwards;
}

Animations are proactive — they start based on a trigger (adding the element to the DOM, applying a class, or running automatically) and can loop, alternate, or play in reverse.

Key Differences

The fundamental difference is scope. Transitions handle the gap between two states. Animations handle complex sequences with any number of states. Transitions are ideal for interactive feedback (hover, focus, active states). Animations are ideal for decorative motion, loading indicators, entrance/exit effects, and any multi-step sequence.

Animation vs transition comparison

Architecture and Design Patterns

The State Change Pattern (Transitions)

This pattern applies when an element has a clear "before" and "after" state, and the user's action triggers the change. Examples include button hover effects, accordion expand/collapse, form field focus states, and toggle switches. The transition bridges the visual gap between states smoothly.

The Entrance Pattern (Animations)

Elements that appear on the page benefit from entrance animations. This includes modals sliding in, notifications fading down from the top, cards appearing in a staggered grid, and page sections revealing on scroll. These use animation-fill-mode: forwards to hold the final state.

The Loading Pattern (Animations)

Loading indicators — spinners, skeleton screens, progress bars — use looping animations. These run continuously until content loads, using animation-iteration-count: infinite or animation-direction: alternate for back-and-forth motion.

The Exit Pattern (Both)

Removing elements from the DOM is tricky because CSS can't transition or animate to display: none. The common pattern uses opacity and transform to animate out, then removes the element via JavaScript after the animation completes. Modern CSS is adding transition-behavior: allow-discrete and @starting-style to address this.

Step-by-Step Implementation

Implementing Button Transitions

A well-designed button uses transitions for multiple interactive states:

.btn {
  padding: 12px 24px;
  background-color: #2563eb;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  cursor: pointer;
  transform: translateY(0);
  box-shadow: 0 2px 4px rgba(37, 99, 235, 0.3);
  transition: 
    background-color 0.15s ease,
    transform 0.15s ease,
    box-shadow 0.15s ease;
}
 
.btn:hover {
  background-color: #1d4ed8;
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(37, 99, 235, 0.4);
}
 
.btn:active {
  background-color: #1e40af;
  transform: translateY(0);
  box-shadow: 0 1px 2px rgba(37, 99, 235, 0.3);
  transition-duration: 0.05s;
}

Building a Loading Spinner with Keyframes

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
 
.spinner {
  width: 40px;
  height: 40px;
  border: 3px solid #e5e7eb;
  border-top-color: #2563eb;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

Creating Staggered Entrance Animations

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
 
.card-grid .card {
  animation: fade-up 0.4s ease-out both;
}
 
.card-grid .card:nth-child(1) { animation-delay: 0s; }
.card-grid .card:nth-child(2) { animation-delay: 0.1s; }
.card-grid .card:nth-child(3) { animation-delay: 0.2s; }
.card-grid .card:nth-child(4) { animation-delay: 0.3s; }

Combining Transitions and Animations

Elements can use both transitions and animations simultaneously for different properties:

.notification {
  /* Animation for entrance */
  animation: slide-down 0.3s ease-out forwards;
  
  /* Transition for interactive states */
  transition: background-color 0.2s ease;
}
 
.notification:hover {
  background-color: #f8fafc;
}
 
@keyframes slide-down {
  from {
    opacity: 0;
    transform: translateY(-100%);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Implementation examples

Real-World Use Cases

Use Case 1: Navigation Menu Transitions

Desktop navigation menus use transitions for dropdown visibility. When the user hovers over a nav item, the dropdown fades in and slides down slightly. The transition timing is fast enough to feel responsive but slow enough to be perceptible. A small delay prevents accidental triggers when the mouse passes over items quickly.

Use Case 2: Page Transition Animations

When navigating between pages in a single-page application, entrance and exit animations create continuity. The outgoing page slides or fades out while the incoming page slides or fades in. This typically requires both CSS animations for the motion and JavaScript to coordinate the timing between page unmount and mount.

Use Case 3: Skeleton Loading Screens

Skeleton screens use subtle shimmer animations to indicate loading content. The shimmer effect is created with a gradient that moves across the skeleton element using a looping CSS animation, providing visual feedback that something is happening without the jarring flash of a spinner appearing and disappearing.

Use Case 4: Micro-Interactions on Form Elements

Form elements benefit from transitions on focus, validation, and error states. Input borders transition color on focus, error messages slide in below invalid fields, and success checkmarks animate in with a draw effect. These micro-interactions guide users through the form completion process.

Best Practices for Production

  1. Prefer transform and opacity for motion — These properties are handled by the compositor thread and don't trigger layout or paint. Animating top, left, width, or height forces the browser to recalculate layout, which is significantly more expensive.

  2. Use appropriate easing functions — ease-out for elements entering the screen (fast start, slow stop). ease-in for elements leaving (slow start, fast exit). ease-in-out for elements moving between visible positions. Avoid linear except for loading spinners.

  3. Keep durations under 300ms for interactions — UI feedback animations (hover, click, toggle) should feel instant. Use 100-200ms for small changes and 200-300ms for larger movements. Longer durations (300-500ms) are appropriate for entrance animations and page transitions.

  4. Respect prefers-reduced-motion — Some users experience motion sickness or distraction from animations. Wrap non-essential animations in a media query check:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
  1. Use will-change sparingly — Only apply will-change to elements that are about to animate, and remove it after the animation completes. Overusing will-change creates unnecessary compositing layers that consume GPU memory.

  2. Avoid animating box-shadow directly — box-shadow is expensive to render. Instead, animate opacity on a pseudo-element with the shadow, or use filter: drop-shadow() which can be hardware-accelerated.

  3. Use animation-fill-mode: forwards carefully — When using forwards, the element retains the animation's final state. Ensure this doesn't conflict with other styles or cause specificity issues.

  4. Test on lower-end devices — Animations that run smoothly on a MacBook Pro may stutter on a mid-range Android phone. Test your motion design on representative devices to ensure a consistent experience.

Common Pitfalls and Solutions

PitfallImpactSolution
Animating top/left instead of transformLayout thrashing, janky animationsUse transform: translate() for position changes
Missing will-change on complex animationsStuttering on first animation frameAdd will-change before animation, remove after
Transition on all propertiesUnintended property transitionsSpecify exact properties in transition-property
No prefers-reduced-motion handlingAccessibility issues for motion-sensitive usersWrap animations in the reduced motion media query
Animation conflicts with transitionErratic behavior when both applyUse different properties for animations and transitions
Forgetting animation-fill-modeElement snaps back to initial stateUse forwards to hold the end state

Performance Optimization

The key to performant CSS motion is understanding the rendering pipeline. When a CSS property changes, the browser may need to run three phases: layout (recalculate geometry), paint (fill in pixels), and composite (layer composition). The cheapest properties to animate are those that only trigger the composite phase — primarily transform and opacity.

For complex animations involving multiple elements, use contain: layout on the parent to limit the scope of layout recalculations. This prevents the browser from recalculating the entire page when one element's layout changes.

.animated-container {
  contain: layout;
}

Use Chrome DevTools' Performance panel to identify animation bottlenecks. Look for long "Recalculate Style" and "Layout" tasks during animation frames. If you see these, the animation is triggering expensive rendering phases and should be optimized.

/* Before: triggers layout + paint + composite */
.sidebar {
  transition: left 0.3s ease;
}
 
/* After: triggers only composite */
.sidebar {
  transform: translateX(0);
  transition: transform 0.3s ease;
}
 
.sidebar.collapsed {
  transform: translateX(-100%);
}

Comparison with Alternatives

FeatureCSS TransitionsCSS AnimationsJavaScript (Web Animations API)GSAP
Multi-step sequencesNoYesYesYes
Loop/alternateNoYesYesYes
Auto-start on loadNoYesYesYes
PerformanceHardware-acceleratedHardware-acceleratedHardware-acceleratedHardware-accelerated
Timing controlBasic (duration, easing)Full (keyframes, delays)FullFull
JavaScript interactionVia class togglesVia class togglesFull programmatic controlFull control
Bundle size0 KB0 KB0 KB~30 KB
Browser supportAll modernAll modernAll modernAll modern

Advanced Patterns

CSS @property for Animating Non-Animatable Properties

The @property rule lets you define custom properties with explicit types, enabling transitions on values that CSS can't normally interpolate:

@property --angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
 
.conic-gradient {
  --angle: 0deg;
  background: conic-gradient(from var(--angle), red, blue);
  transition: --angle 0.5s ease;
}
 
.conic-gradient:hover {
  --angle: 180deg;
}

View Transitions API

The View Transitions API provides a higher-level mechanism for animating between DOM states, particularly useful for page transitions in single-page applications:

::view-transition-old(root) {
  animation: fade-out 0.2s ease-out;
}
 
::view-transition-new(root) {
  animation: fade-in 0.3s ease-in;
}

Scroll-Driven Animations

Modern CSS allows animations to be driven by scroll position rather than time, enabling parallax effects, progress indicators, and reveal-on-scroll patterns without JavaScript:

@keyframes reveal {
  from { opacity: 0; transform: translateY(30px); }
  to { opacity: 1; transform: translateY(0); }
}
 
.section {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

Testing Strategies

Automated testing of CSS animations requires both visual and behavioral verification:

test('button hover transition activates within 200ms', async ({ page }) => {
  await page.goto('/components/button');
  const button = page.locator('.btn');
  
  const initialBg = await button.evaluate(
    el => getComputedStyle(el).backgroundColor
  );
  
  await button.hover();
  
  // Wait for transition to complete
  await page.waitForTimeout(250);
  
  const hoverBg = await button.evaluate(
    el => getComputedStyle(el).backgroundColor
  );
  
  expect(initialBg).not.toBe(hoverBg);
});
 
test('reduced motion disables animations', async ({ page }) => {
  await page.emulateMedia({ reducedMotion: 'reduce' });
  await page.goto('/components/spinner');
  
  const duration = await page.locator('.spinner').evaluate(
    el => getComputedStyle(el).animationDuration
  );
  
  expect(parseFloat(duration)).toBeLessThanOrEqual(0.01);
});

Future Outlook

CSS is evolving to make motion even more powerful and easier to implement. The View Transitions API enables seamless page transitions without JavaScript animation libraries. Scroll-driven animations eliminate the need for JavaScript scroll listeners. The @starting-style rule and transition-behavior property solve the long-standing problem of animating display changes. These advances are pushing more animation logic from JavaScript into the browser's native rendering pipeline.

Cross-Browser Testing Strategy

Modern CSS features often have varying levels of browser support, making a systematic cross-browser testing strategy essential. Before using any CSS feature in production, verify its support status and implement appropriate fallbacks for browsers that haven't yet implemented it.

Progressive Enhancement with @supports

Use the @supports at-rule to provide fallback styles for browsers that don't support specific CSS features:

/* Base styles for all browsers */
.grid-container {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}
 
.grid-container > * {
  flex: 1 1 300px;
}
 
/* Enhanced layout for browsers supporting CSS Grid */
@supports (display: grid) {
  .grid-container {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 1.5rem;
  }
 
  .grid-container > * {
    flex: none;
  }
}
 
/* Further enhancement with subgrid support */
@supports (grid-template-columns: subgrid) {
  .grid-container {
    grid-template-columns: subgrid;
  }
}

Visual Regression Testing

Implement visual regression testing to catch unintended layout shifts and styling changes. Tools like Percy, Chromatic, or Playwright's screenshot comparison can detect visual differences across browsers and screen sizes:

const { test, expect } = require('@playwright/test');
 
test('responsive layout matches design', async ({ page }) => {
  await page.goto('/components/dashboard');
 
  // Test at multiple viewport sizes
  for (const viewport of [
    { width: 375, height: 812, name: 'mobile' },
    { width: 768, height: 1024, name: 'tablet' },
    { width: 1440, height: 900, name: 'desktop' },
  ]) {
    await page.setViewportSize({ width: viewport.width, height: viewport.height });
    await expect(page).toHaveScreenshot(
      `dashboard-${viewport.name}.png`,
      { maxDiffPixels: 100 }
    );
  }
});

Browser Compatibility Testing Matrix

Maintain a testing matrix that covers the browsers and versions your users actually use. Use analytics data to determine your browser support baseline, then configure tools like Browserslist to automatically handle polyfilling and prefixing:

{
  "browserslist": [
    "> 0.5%",
    "last 2 versions",
    "not dead",
    "not ie 11"
  ]
}

This data-driven approach ensures you're spending testing effort where it matters most, rather than trying to support every possible browser configuration.

Community Resources and Further Learning

The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.

Curated Learning Pathways

Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.

Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.

Contributing to Open Source

Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.

# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
 
# Run the project's contribution setup
npm run setup:dev
npm run test  # Ensure tests pass before making changes
 
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
 
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
 
Closes #1234"
git push origin fix/issue-description

Building a Technical Knowledge Base

Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.

Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.

Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.

Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.

Mentorship and Knowledge Sharing

Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.

Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.

Conclusion

CSS transitions and animations serve complementary roles in web motion design. The key takeaways are:

  1. Use transitions for state changes — hover, focus, toggle, and class changes that bridge two states with smooth motion.
  2. Use animations for complex sequences — multi-step motion, looping indicators, entrance effects, and anything that needs keyframe control.
  3. Always animate transform and opacity — these properties are hardware-accelerated and avoid expensive layout recalculations.
  4. Respect user preferences — wrap non-essential animations in prefers-reduced-motion media queries for accessibility.
  5. Test performance on real devices — smooth 60fps motion on a desktop browser doesn't guarantee the same on mobile hardware.

Start with transitions for interactive feedback, add animations for entrance effects and loading states, and always prioritize performance by choosing the right properties to animate.