DEV Community

Fazliddin
Fazliddin

Posted on • Edited on

React-Router v6: Animated Transitions DIY

Thanks to Anxin.Y for the article about making a transition DIY through animation on React-router v5.

Now, I will try to make it with v6.

So, Let's start!

First, let's create the App component:

export default function App() {
  return (
    <BrowserRouter>
      <div className={`App`}>
        <nav>
          <Link to="/">Home</Link>
          <Link to="/other">Other</Link>
        </nav>
        <Content />
      </div>
    </BrowserRouter>
  );
}
Enter fullscreen mode Exit fullscreen mode

Then, the Content component:

function Content() {
    return (
    <div>
      <Routes>
        <Route path="/" element={<section>Home</section>} />
        <Route path="/other" element={<section>Other</section>} />
      </Routes>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Now, we need to stop route from switching. By default, the <Routes /> (in v5, <Switch />) will use the current url for matching the route, but we can stop it from doing that by assigning it a Location.

<Routes location={displayLocation}>
Enter fullscreen mode Exit fullscreen mode

We will need a state to keep the current location before 'Out' animation finish, and we can assign the current location as the default value. we can use useLocation to get the current location.

  ...
  const location = useLocation();
  const [displayLocation, setDisplayLocation] = useState(location);
  ...
  <Routes location={displayLocation}>
Enter fullscreen mode Exit fullscreen mode

Now, if you click the Link, you will notice that even the URL is changed, the content stay the same.

Next, we need add a state for controlling the stage of the transition.

const [transitionStage, setTransistionStage] = useState("fadeIn");
Enter fullscreen mode Exit fullscreen mode

Then, we can use useEffect to check if location is changed, and start the 'fadeOut'.

  useEffect(() => {
    if (location !== displayLocation) setTransistionStage("fadeOut");
  }, [location, displayLocation]);
Enter fullscreen mode Exit fullscreen mode

Finally, we need a way to update the stage and location when animation is over. For that we can use the onAnimationEnd event.

function Content() {
  ...
  return (
    <div
      className={`${transitionStage}`}
      onAnimationEnd={() => {
        if (transitionStage === "fadeOut") {
          setTransistionStage("fadeIn");
          setDisplayLocation(location);
        }
      }}
    >
      ...
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Before completion, You must add these to your CSS:

.fadeIn {
  animation: 0.5s fadeIn forwards;
}

.fadeOut {
  animation: 0.5s fadeOut forwards;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translate(-20px, 0);
  }
  to {
    opacity: 1;
    transform: translate(0px, 0px);
  }
}

@keyframes fadeOut {
  from {
    opacity: 1;
    transform: translate(0px, 0px);
  }
  to {
    transform: translate(-20px, 0);
    opacity: 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

And, Here is the demo and the finished code:


import { useState, useEffect } from "react";
import {
  BrowserRouter,
  Link,
  useLocation,
  Route,
  Routes
} from "react-router-dom";
import "./styles.css";

export default function App() {
  return (
    <BrowserRouter>
      <div className={`App`}>
        <nav>
          <Link to="/">Home</Link>
          <Link to="/other">Other</Link>
        </nav>
        <Content />
      </div>
    </BrowserRouter>
  );
}

function Content() {
  const location = useLocation();

  const [displayLocation, setDisplayLocation] = useState(location);
  const [transitionStage, setTransistionStage] = useState("fadeIn");

  useEffect(() => {
    if (location !== displayLocation) setTransistionStage("fadeOut");
  }, [location, displayLocation]);

  return (
    <div
      className={`${transitionStage}`}
      onAnimationEnd={() => {
        if (transitionStage === "fadeOut") {
          setTransistionStage("fadeIn");
          setDisplayLocation(location);
        }
      }}
    >
      <Routes location={displayLocation}>
        <Route path="/" element={<section>Home</section>} />
        <Route path="/other" element={<section>Other</section>} />
      </Routes>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Thank you AnxinYang!

Top comments (12)

Collapse
 
fredleput profile image
fredleput • Edited

If i may, i was inspired by ur post and i tried to make it more flexible...

MainContent.js :

import React, { useState, useEffect, Component } from 'react';
import { Routes, Route, useLocation } from "react-router-dom";

import Presentation from './Presentation';
import Horaires from './Horaires';
import Tarifs from './Tarifs';

const roads = [
    {
        path: '/',
        id: 'presentation',
        animation: 'presentation',
        Component: Presentation
    },
    {
        path: '/horaires',
        id: 'horaires',
        animation: 'horaires',
        Component: Horaires
    },
    {
        path: '/tarifs',
        id: 'tarifs',
        animation: 'tarif',
        Component: Tarifs
    }
]

const MainContent= () => {
    // use location hook
    const location = useLocation();

    // location local state
    const [displayLocation, setDisplayLocation] = useState(location);

    // transitions state
    const [transitionStage, setTransistionStage] = useState("fadeIn");

    useEffect(() => {
        if (location !== displayLocation) setTransistionStage("fadeOut");
    }, [location, displayLocation]);

    const handleAnimationEnd = (event) => {
        if (transitionStage === "fadeOut") {
            setTransistionStage("fadeIn");
            setDisplayLocation(location);
        }
    }

    return(    
        <> 
        {
            roads.map((Road, index) => (
                <div
                    key={index}
                    id={Road.id}
                    className={`${transitionStage}`}
                    onAnimationEnd={handleAnimationEnd}
                >
                    <Routes location={displayLocation}>
                        <Route exact path={Road.path} element={<Road.Component />} />
                    </Routes>
                </div>
            ))
        }
        </>
    );
};

export default MainContent;
Enter fullscreen mode Exit fullscreen mode

Styles for animations

@import url('https://fonts.googleapis.com/css2?family=Lobster+Two:wght@700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@300&display=swap');

html, body {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
}

body {
    background: url(../images/le-petit-train-paimpolais.jpg) no-repeat center center fixed; 
    -webkit-background-size: cover;
    -moz-background-size: cover;
    -o-background-size: cover;
    background-size: cover;
}

#header {
    position: absolute;
    left: 20px;
    top: 20px;
    background-color: rgba(255,255,255,0.75);
    padding: 15px;
    margin: 0;
    display: grid;
    grid-template-columns: 1fr auto;
    max-width: 80%;
}

#header #main-title {
    margin-right: 15px;
    padding-right: 16px;
    position: relative;
}

#header #main-title h1, h2 {
    margin: 0;
    line-height: 42px;
    font-family: 'Lobster Two', cursive;
    color: #0e27fd;
}

#header #main-nav {
    display: grid;
    grid-template-columns: repeat(6, auto);
    align-items: center;
    justify-items: center;
    justify-content: center;
}

#header #main-nav .btn {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 0;
    display: grid;
    align-items: center;
    justify-content: center;
    background-color: rgba(255, 255, 255, 0.5);
    border: 1px solid #f6ed16;
    transition: all 0.4s ease;
}

#header #main-nav .btn:hover {
    transform: scale(1.5);
    background-color: rgba(33, 33, 33, 0.4);
    border: 1px solid #0e27fd;
}

#header #main-nav .btn:not(:last-child) {
    margin-right: 15px;
}

#header #main-nav .btn svg {
    width: 24px;
    height: 24px;
    color: #0e27fd;
}

#header #main-nav .btn:hover svg {
    color: #f6ed16;
}

.content {
    padding: 20px;
}

.content-text {
    font-family: 'Merriweather', serif;
    color: #000;
    font-size: 1.1em;
    line-height: 1.4;
}

.content-text *:last-child {
    margin-bottom: 0;
}

.sepa-v {
    position: absolute;
    right: 0;
    top: 0;
    width: 1px;
    height: 100%;
    background: rgb(255,255,255);
    background: linear-gradient(0deg, rgba(255,255,255,0) 0%, rgba(246,237,22,1) 10%, rgba(246,237,22,1) 90%, rgba(255,255,255,0) 100%); 
}

.sepa-h {
    width: 90%;
    margin: 20px auto;
    height: 1px;
    background: rgb(255,255,255);
    background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(246,237,22,1) 10%, rgba(246,237,22,1) 90%, rgba(255,255,255,0) 100%); 
}

.rc-tooltip-placement-bottom .rc-tooltip-arrow, .rc-tooltip-placement-bottomLeft .rc-tooltip-arrow, .rc-tooltip-placement-bottomRight .rc-tooltip-arrow {
    border-bottom-color: #333;
}

.rc-tooltip-inner {
    border: 0;
    border-radius: 4px;
    background-color: #333;
    color: #fff;
    min-height: auto;
    font-size: 1.4em;
}

/* transition animations on blocks */
#presentation {
    position: absolute;
    top: 20px;
    right: 20px;
    background-color: rgba(255, 255, 255, 0.85);
    width: 480px;
}

#presentation.fadeIn {
    animation: 0.4s presentationFadeIn forwards;
}

#presentation.fadeOut {
    animation: 0.4s presentationFadeOut forwards;
}

@keyframes presentationFadeIn {
    from {
        opacity: 0;
        transform: translate(-20px, -20px);
        transform: scale(0);
        transform-origin: top right;
    }
    to {
        opacity: 1;
        transform: translate(0px, 0px);
        transform: scale(1);
    }
}

@keyframes presentationFadeOut {
    from {
        opacity: 1;
        transform: translate(0, 0);
        transform: scale(1);
        transform-origin: bottom right;
    }
    to {
        opacity: 0;
        transform: translate(-20px, -20px);
        transform: scale(0);
    }
}

#tarifs {
    position: absolute;
    top: 200px;
    left: 20px;
    background-color: rgba(255, 255, 255, 0.85);
    width: 480px;
}

#tarifs.fadeIn {
    animation: 0.4s tarifsFadeIn forwards;
}

#tarifs.fadeOut {
    animation: 0.4s tarifsFadeOut forwards;
}

@keyframes tarifsFadeIn {
    from {
        opacity: 0;
        transform: translate(-20px, -20px);
        transform: scale(0);
        transform-origin: top left;
    }
    to {
        opacity: 1;
        transform: translate(0px, 0px);
        transform: scale(1);
    }
}

@keyframes tarifsFadeOut {
    from {
        opacity: 1;
        transform: translate(0, 0);
        transform: scale(1);
        transform-origin: bottom left;
    }
    to {
        opacity: 0;
        transform: translate(-20px, -20px);
        transform: scale(0);
    }
}

#horaires {
    position: absolute;
    top: 200px;
    left: 200px;
    background-color: rgba(255, 255, 255, 0.85);
    width: 480px;
}

#horaires.fadeIn {
    animation: 0.4s horairesFadeIn forwards;
}

#horaires.fadeOut {
    animation: 0.4s horairesFadeOut forwards;
}

@keyframes horairesFadeIn {
    from {
        opacity: 0;
        transform: translate(-20px, -20px);
        transform: scale(0);
        transform-origin: top left;
    }
    to {
        opacity: 1;
        transform: translate(0px, 0px);
        transform: scale(1);
    }
}

@keyframes horairesFadeOut {
    from {
        opacity: 1;
        transform: translate(0, 0);
        transform: scale(1);
        transform-origin: bottom left;
    }
    to {
        opacity: 0;
        transform: translate(-20px, -20px);
        transform: scale(0);
    }
}
Enter fullscreen mode Exit fullscreen mode

Regards,
Fred, Brittany - France

Collapse
 
juanestban profile image
Juan Esteban Perdomo

YOU ARE AMAIZING! >.<

Collapse
 
brianmcbride profile image
Brian McBride

An issue here is that you are only displaying one view at a time.
What happens if you want to animate a route, mobile/native style, where one view slides in over another?

I am on something close here. The new React Router 6 has useOutlet where it will give you the current element in that outlet that that you are rendering.
You can store that element stack in a ref and then when the navigation changes, use that stashed ref (which will be the old outlet results as the outlet will be updated) and then you render that under your new outlet with your animation triggering over.

Obviously, if you have nested outlets and a mixture of tabs/menus/stacks your back-navigation will get tricky. Still, you it should be on the right direction.

Collapse
 
kirusha05 profile image
Kirusha05

Thank you!!! This is exactly what I needed, a DYI approach. Found some tutorials, but they needed Framer Motion or another libraries... Learned something new today 😅

Collapse
 
hendrikras profile image
Hendrik Ras

Thank you! Love how you included a working code sandbox. It's very useable!

Collapse
 
pushon profile image
Pushon

can you update this for the v6.4 + syntex ?

Collapse
 
caducoder profile image
Carlos Eduardo

How can I do that with nested routes?

Collapse
 
andreweastwood profile image
Andrew Eastwood

I like it!!!

Collapse
 
ehylam profile image
Eric

Thanks for this article! With what I've learnt from this article I was able to create page transitions with GSAP.

Collapse
 
juank060790 profile image
JuanCa

This is aweesome, thanks. Just I have the same code as you but i get an error as it renders multiple times, is there any fix to it? or am i dooing something wrong.

Again thank you

Collapse
 
cxrloskenobi profile image
Carlos • Edited

Wow dude, I came from using react-transition-group, but since react router v6 arrived, this beautiful method is my salvation. Nice work of @anxiny and yours!

Collapse
 
anxiny profile image
Anxiny • Edited

Thanks! Glad I can help 😊. Nice article!