How to animate react-router routes in 2019

The sane mans method to getting those silky route transitions.

23 October 2019

Animating between routes using react-router, for me, has always been a pain in the backside. The awful react-transition-group documentation, coupled with out-of-date blog posts and difficulty in debugging issues, at times left me wanting to take a hammer to my monitor.

I've not liked some of the existing solutions because they use absolute positioning to achieve cross-fades between routes (which have caused more headaches e.g. FOUC), or they do not cater for the fact that content may be lazy loaded. I'm not proclaiming that my solution trumps all, but it has worked for me in both personal and client projects.

This post assumes that you are:

  • using a recent version of react (16.10.2), react-router (5.1.2) and react-transition-group (4.3.0) - bracketed versions are what I used at the time of writing this article
  • lazy-loading your route components (although this method will work without lazy-loading)
  • are not looking to cross-fade your route transitions and fading out the first route before animating in the next one
  • building your app in TypeScript - if not, it's straightforward enough to convert into regular JS
  • using a browser that wasn't built in the 18th century


In your App.tsx or similar entry point for your routes, enclose your Switch component inside the TransitionGroup and CSSTransition components like so:

export const App: React.FC = () => {
   const location = useLocation();
   return (
     <div className="App">
           timeout={{ enter: 1000, exit: 200 }}>
           <Suspense fallback={<Loading />}>
             <Switch location={location}>
                 path="/" exact
                 component={React.lazy(() => import('./Home/HomeRoute'))}
                 component={React.lazy(() => import('./Test/TestRoute'))}

Note that we have set the timeout prop, which is defining how long we want the 'enter' and 'exit' animations to take. In this case, entering will take 1s long and exiting 0.2s. This will need to match up to the animation-duration that you will define in your SCSS later.


I usually keep global styles in a root-level styles folder (component styles live alongside the component). This contains basic site setup, SCSS variables, mixins, and utility/helper classes. I also like to prefix any animation utility classes with 'a-'.

.a-routeFadeIn {
   animation: a-routeFadeIn 0.75s ease-in-out 0.25s;
   animation-fill-mode: both;

 @keyframes a-routeFadeIn {
   0% {
     opacity: 0;
     transform: translateY(30px);
     height: 0;
     overflow: hidden;
   0.01% {
     height: auto;
     overflow: visible;
   100% {
     opacity: 1;
     transform: translateY(0);

 .a-routeFadeIn.fade-exit {
   animation: none;
   animation-fill-mode: none;
   opacity: 1;
   transition: opacity 0.2s ease-in-out;

 .a-routeFadeIn.fade-exit-active {
   opacity: 0;

The reason why animation is used over transition is threefold:

  • Transitions do not execute if the class is already applied when the component mounts. Animations, on the other hand, run perfectly even if the class is already applied at mount time.
  • The transitions could have already started whilst the route component is being lazy-loaded- we want to delay executing the transition until the route has been fetched.
  • the CSSTransition component will apply the fade-enter and fade-enter-active classes to the Suspense fallback component instead of the Route component (because the fallback component will be shown whilst the route component is being fetched).

Finally, you can see the fade-in animation takes 1s in total to complete (0.75s transition + 0.25s delay). We delay by 0.25s to give the exit animation (0.2s duration) enough time to complete, before we start our entry animation.


All that is left to do now is apply our animation utility class to each of your routes.

export const HomeRoute: React.FC<RouteProps> = (props) => {
   return (
     <div className="HomeRoute a-routeFadeIn">

This animation will execute as soon as your route component has been lazy loaded and mounted.

I hope this helps somebody out - let me know on Twitter (@romiem) if you found this helpful or are using a similar solution!

Recent posts

Adding Opengraph to your single page app

The easy way to add social media link previews to a serverless web app.

Photo Series: Dreamstate

Devon (UK) is always a great place for photography in the summer; these were taken on the southern coast, close to Exeter.

Photo Series: Indian Nights

These photos were taken on a recent holiday to India.