coderain blog

How to Animate Route Transitions with CSSTransitionGroup in React-Router v6: Solving Compatibility Issues with the New API

Smooth route transitions are a cornerstone of modern web applications, enhancing user experience by providing visual feedback during navigation. For years, developers have relied on libraries like react-transition-group (specifically CSSTransitionGroup) to animate route changes in React apps. However, with the release of React-Router v6, significant API changes—such as the introduction of Routes/Route components, the removal of Switch, and the element prop—broke many existing transition implementations.

If you’ve struggled to get route animations working with React-Router v6, you’re not alone. This blog will demystify the process, address compatibility issues, and guide you through step-by-step to create seamless route transitions using react-transition-group (the modern successor to CSSTransitionGroup). By the end, you’ll have a clear understanding of how to integrate CSS transitions with React-Router v6 and troubleshoot common pitfalls.

2025-12

Table of Contents#

  1. Prerequisites
  2. Understanding React-Router v6 Changes
  3. Setting Up react-transition-group
  4. Integrating Transitions with React-Router v6
  5. Creating CSS Transition Classes
  6. Handling Edge Cases
  7. Complete Example
  8. Common Issues and Solutions
  9. Conclusion
  10. References

Prerequisites#

Before diving in, ensure you have the following:

  • A React project set up (v16.8+ for Hooks support).
  • react-router-dom v6 installed (npm install react-router-dom@6).
  • Basic knowledge of React Hooks (e.g., useState, useEffect, useLocation).
  • Familiarity with CSS transitions/animations.

We’ll also use react-transition-group (v4+), the official library for managing component transitions in React.

Understanding React-Router v6 Changes#

To solve compatibility issues, it’s critical to understand what changed in React-Router v6:

v5 and Earlierv6Impact on Transitions
Switch componentReplaced with RoutesRoutes renders the first matching Route, but unlike Switch, it does not unmount components by default.
component/render propsReplaced with element propRoutes now render components via <Route path="/" element={<Home />} />.
useHistory hookReplaced with useNavigateNavigation is now handled via navigate(), but location tracking still uses useLocation.

The biggest challenge for transitions is that Routes does not automatically unmount inactive routes. Without unmounting, react-transition-group can’t detect when to trigger exit animations. We’ll solve this by combining useLocation (to track route changes) with TransitionGroup (to manage enter/exit states).

Setting Up react-transition-group#

CSSTransitionGroup was deprecated in react-transition-group v2. Today, we use TransitionGroup (to manage a list of transitioning components) and CSSTransition (to apply CSS classes during transitions).

First, install the library:

npm install react-transition-group@4  # v4 is stable and widely used

Import the required components in your app:

import { TransitionGroup, CSSTransition } from 'react-transition-group';

Integrating Transitions with React-Router v6#

The core idea is to wrap Routes in TransitionGroup and use CSSTransition to animate route entries/exits. Here’s a step-by-step breakdown:

Step 1: Track Location Changes with useLocation#

useLocation returns the current location object, which updates whenever the route changes. We’ll use this to trigger transitions:

import { useLocation } from 'react-router-dom';
 
function App() {
  const location = useLocation(); // Tracks current route
  // ...
}

Step 2: Wrap Routes in TransitionGroup#

TransitionGroup manages the lifecycle of transitioning components. Each child must have a unique key to trigger re-renders when the route changes. We’ll use location.pathname as the key (unique per route):

<TransitionGroup>
  {/* CSSTransition wraps the Routes */}
  <CSSTransition
    key={location.pathname}  {/* Unique key for each route */}
    timeout={300}            {/* Duration of enter/exit animations (ms) */}
    classNames="fade"        {/* Prefix for CSS classes (e.g., fade-enter) */}
    unmountOnExit            {/* Unmount component after exit animation */}
  >
    {/* Pass location to Routes to ensure it re-renders on route change */}
    <Routes location={location}>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/contact" element={<Contact />} />
    </Routes>
  </CSSTransition>
</TransitionGroup>

Key Props Explained:#

  • key={location.pathname}: Ensures TransitionGroup recognizes route changes as new children, triggering exit for the old route and enter for the new.
  • timeout: Matches the duration of your CSS transitions (e.g., 300ms for a fade).
  • classNames: Prefix for CSS classes (e.g., fade-enter, fade-exit-active).
  • unmountOnExit: Unmounts the old route component after the exit animation completes (critical for triggering transitions).
  • location={location}: Forces Routes to re-render when the location changes, ensuring the new route content loads.

Creating CSS Transition Classes#

CSSTransition adds/removes CSS classes at specific stages of the transition. For a classNames="fade" prefix, define these classes:

ClassTimingPurpose
fade-enterApplied immediately when entering.Define the initial state (e.g., opacity: 0).
fade-enter-activeApplied after enter (with a timeout).Define the final state (e.g., opacity: 1) and transition properties.
fade-exitApplied immediately when exiting.Define the initial exit state (e.g., opacity: 1).
fade-exit-activeApplied after exit (with a timeout).Define the final exit state (e.g., opacity: 0) and transition properties.

Example CSS (save as App.css):

/* Fade transition */
.fade-enter {
  opacity: 0;
  transform: translateY(20px); /* Optional: Add slide effect */
}
 
.fade-enter-active {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 300ms, transform 300ms; /* Match CSSTransition timeout */
}
 
.fade-exit {
  opacity: 1;
}
 
.fade-exit-active {
  opacity: 0;
  transition: opacity 300ms;
}

Handling Edge Cases#

1. Disabling Initial Mount Animation#

By default, the first route will trigger an enter animation on app load. To disable this:

const [isMounted, setIsMounted] = useState(false);
 
useEffect(() => {
  // Skip animation on initial mount
  const timer = setTimeout(() => setIsMounted(true), 10);
  return () => clearTimeout(timer);
}, []);
 
// Pass `in={isMounted}` to CSSTransition
<CSSTransition
  key={location.pathname}
  in={isMounted}  {/* Only animate after initial mount */}
  timeout={300}
  classNames="fade"
  unmountOnExit
>
  {/* ... */}
</CSSTransition>

2. Directional Transitions (e.g., Slide Left/Right)#

To animate based on navigation direction (forward/back), use location.state to track direction:

Step 1: Pass direction in navigation links:

import { Link } from 'react-router-dom';
 
<Link to="/about" state={{ direction: 'forward' }}>About</Link>
<Link to="/" state={{ direction: 'back' }}>Home</Link>

Step 2: Dynamically set classNames based on direction:

const { state } = location;
const direction = state?.direction || 'forward'; // Default to forward
 
<CSSTransition
  key={location.pathname}
  classNames={direction === 'forward' ? 'slide-right' : 'slide-left'}
  timeout={300}
  unmountOnExit
>
  {/* ... */}
</CSSTransition>

Step 3: Add CSS for slide-right and slide-left:

/* Slide right (forward navigation) */
.slide-right-enter { transform: translateX(100%); }
.slide-right-enter-active { transform: translateX(0); transition: transform 300ms; }
.slide-right-exit { transform: translateX(0); }
.slide-right-exit-active { transform: translateX(-100%); transition: transform 300ms; }
 
/* Slide left (back navigation) */
.slide-left-enter { transform: translateX(-100%); }
.slide-left-enter-active { transform: translateX(0); transition: transform 300ms; }
.slide-left-exit { transform: translateX(0); }
.slide-left-exit-active { transform: translateX(100%); transition: transform 300ms; }

Complete Example#

Here’s the full App.js integrating all the above:

import { useState, useEffect } from 'react';
import { Routes, Route, useLocation, Link } from 'react-router-dom';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import './App.css';
 
// Sample pages
const Home = () => <h1>Home</h1>;
const About = () => <h1>About</h1>;
const Contact = () => <h1>Contact</h1>;
 
function App() {
  const location = useLocation();
  const [isMounted, setIsMounted] = useState(false);
 
  // Disable initial animation
  useEffect(() => {
    const timer = setTimeout(() => setIsMounted(true), 10);
    return () => clearTimeout(timer);
  }, []);
 
  return (
    <div>
      {/* Navigation */}
      <nav>
        <Link to="/" state={{ direction: 'back' }}>Home</Link> |
        <Link to="/about" state={{ direction: 'forward' }}>About</Link> |
        <Link to="/contact" state={{ direction: 'forward' }}>Contact</Link>
      </nav>
 
      {/* Transition wrapper */}
      <TransitionGroup>
        <CSSTransition
          key={location.pathname}
          in={isMounted}
          timeout={300}
          classNames={location.state?.direction === 'back' ? 'slide-left' : 'slide-right'}
          unmountOnExit
        >
          <Routes location={location}>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/contact" element={<Contact />} />
          </Routes>
        </CSSTransition>
      </TransitionGroup>
    </div>
  );
}
 
export default App;

Common Issues and Solutions#

IssueSolution
Transitions not triggeringEnsure key={location.pathname} is set on CSSTransition, and location is passed to Routes.
Exit animations not runningAdd unmountOnExit to CSSTransition to unmount inactive routes.
CSS classes not applyingVerify classNames prop matches your CSS prefix (e.g., classNames="fade".fade-enter).
Initial mount animationUse the isMounted state trick to skip the first animation.

Conclusion#

By combining react-transition-group with React-Router v6’s useLocation and Routes, you can create smooth, customizable route transitions. The key steps are:

  1. Track route changes with useLocation.
  2. Wrap Routes in TransitionGroup and CSSTransition.
  3. Define CSS classes for enter/exit states.
  4. Handle edge cases like initial mounts and directional animations.

With these tools, you can elevate your app’s UX and keep up with React-Router’s evolving API.

References#