Back

Multi-step forms with Transition Effects in React

Multi-step forms with Transition Effects in React

A multi-step form is a long form that is broken into multiple steps to make it look less intimidating to site visitors, allowing them to complete their information in chunks. It helps to provide a user-friendly and well-organized layout. In this article, we will learn how to create a multi-step form with a transition animation. To follow up with this tutorial, you should be familiar with React and React hooks.

File Architecture

Before diving into the codes, let’s have a look at how our folders and files will be structured.

src
  ┣ components
  ┃ ┣ 0therInfo.jsx
  ┃ ┣ LocationInfo.jsx
  ┃ ┣ PersonalInfo.jsx
  ┃ ┗ SignUp.jsx
  ┣ App.css
  ┣ App.js 
  ┣ index.css
  ┗ index.js

We have a components folder that contains the OtherInfo.jsx, LocationInfo.jsx, PersonalInfo.jsx and SignUp.jsx. These files contain inputs for different form sections in our app.

Creating Multi-Step Form App

Let’s start by setting up a new React app using the npx or yarn command below.

npx create-react-app multi-step-form-app   
npm start
// OR
yarn create react-app multi-step-form-app
yarn start

Here is a GIF showing what we will be building in the following sections:

1

Create the Form components

We’ll be splitting our codes into smaller form components for each form to be displayed. Let’s start by creating a components folder in the src directory. Afterward, create a SignUp.jsx file in the src/component folder and paste the code snippet into it.

const SignUp = () => {
  return (
    <div className="card">
      <div className="step-title">Sign Up</div> 
      <input
        type="text"
        placeholder="Full Name"
        className="form-group"
      />
      <input
        type="text"
        className="form-group"
        placeholder="Username"
      />
      <input
        type="text"
        className="form-group"
        placeholder="Password"
      />
      <button>
        Next
      </button>
    </div>
  );
};

export default SignUp;

In the above code, we create a form using the input tag and setting the placeholder to the input description. We’ll be doing the same for the next three (3) Form components( PersonalInfo.jsx, LocationInfo.jsx, OtherInfo.jsx). Create a PersonalInfo.jsx file in the src/components folder and paste the code snippet into it.

const PersonalInfo = () => {
  return (
    <div className="card">
      <div className="step-title">Personal Info</div>
      <input
        type="text"
        placeholder="Nickname"
      />
      <input
        type="text"
        placeholder="Email"
      />
      <button>
        Next
      </button>
      <br/>
      <button>
        Previous
      </button>
    </div>
  );
};

export default PersonalInfo

Create a LocationInfo.jsx file in the src/components folder and paste the code snippet into it.

const LocationInfo = () => {
  return (
    <div className="card">
      <div className="step-title">Location Info</div>
      <input
        type="text"
        placeholder="Address"
      />
      <input
        type="text"
        placeholder="Nationality"
      />
      <input
        type="text"
        placeholder="Zipcode"
      />

      <button>
        Next
      </button>
      <br />
      <button>
        Previous
      </button>
    </div>
  );
};
  
export default LocationInfo;

Finally, create an OtherInfo.jsx file in the src/components folder and paste the code snippet into it.

const OtherInfo = () => {
  return (
    <div className="card">
      <div className="step-title">Other Info</div>
      <input
        type="text"
        placeholder="Highest Qualification"
      />
      <input
        type="text"
        placeholder="Occupation"
      />
      <textarea
        type="text"
        placeholder="About"
      />
      <br />
      <button> 
        Submit
      </button>
      <br />
      <button>
        Previous
      </button>
    </div>
  );
};

export default OtherInfo;

With this, we have created the layout of the forms we will use to create the functionality of the multi-step form. To finish, replace the entire code in the App.js file with the code block below.

function App() {
  return (
    <div className="App">
      <div className="progress-bar"></div>
      <div>Forms</div>
    </div>
  );
}

The code block above is the template for our app. We have the progress-bar, and the div that will contain our forms later on.

Styling our Forms

Let’s add some nice colors and displays to our form. Head over to the App.css file and replace everything with the code below.

.App {
  overflow: hidden;
  display: flex;
  justify-content: center;
  flex-direction: column;
  align-items: center;
  position: relative;
  height: 100vh;
  background: rgba(194, 233, 251, 1);
  background: radial-gradient(
    circle,
    rgba(161, 196, 253, 1) 10%,
    rgba(194, 233, 251, 1) 100%
  );
}
.progress-bar {
  width: 400px;
  height: 15px;
  background-color: white;
  margin-bottom: 50px;
}
.progress-bar div {
  width: 25%;
  height: 100%;
  background-color: rgb(98, 0, 255);
}
.card {
  width: 400px;
  background-color: white;
  border: 1px solid #333;
  border-radius: 0.5rem;
  padding: 0.5rem;
  max-width: 500px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
}
.form-group {
  display: flex;
  flex-direction: column;
  margin-bottom: 0.5rem;
  gap: 0.25em;
}
.form-group:last-child {
  margin: 0;
}
.form-group > label {
  font-weight: bold;
  font-size: 0.8em;
  color: #333;
}
.form-group > input {
  border: 1px solid #333;
  border-radius: 0.25em;
  font-size: 1rem;
  padding: 0.25em;
}
.step-title {
  margin: 0;
  margin-bottom: 1rem;
  font-size: 2rem;
  text-align: center;
}
input {
  border: 2px solid rgb(98, 0, 255);
  border-radius: 5px;
  height: 35px;
  display: block;
  margin-bottom: 1rem;
}
.button-area {
  display: flex;
  align-content: space-between;
}
button {
  background-color: #4caf50;
  border: none;
  color: white;
  padding: 0.8rem;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
}
textarea {
  height: 5rem;
}

The styles above will add a nice aesthetic feel to our form, making it more appealing to our users.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Creating dynamic pages

In this section, we’ll display our forms dynamically upon clicking the Next and Previous buttons. By doing this, we will be manipulating the a state property. Import the useState hook from the react library into the App.js file.

import { useState } from "react";

Instantiate the useState hook in the App() component.

function App() {
  const [page, setPage] = useState(0);

  ...
}

In the code block above, we created a page hook and the setPage for updating the page hook while setting the initial state to zero (first form) in the useState() function. Import the components created earlier into the App.js file.

import SignUp from "./components/SignUp";
import PersonalInfo from "./components/PersonalInfo";
import LocationInfo from "./components/LocationInfo";
import OtherInfo from "./components/OtherInfo";

Then, paste the code below into the App() component.

function App() {
  const [page, setPage] = useState(0);

  const componentList = [
    <SignUp />,
    <LocationInfo />,
    <PersonalInfo />,
    <OtherInfo />,
  ];

  ...
}

In the above code, we created two arrays, the componentList array that contains the list of all our forms in the order we want them to display. Next, let’s update our User Interface with our dynamic form. Update the return() in the App.js with the code below.

return (
    <div className="App">
    <div className="progress-bar"></div>       
    <div>{componentList[page]}</div>
  </div>
);

In the above code, we display our componentList using the page value as our index.

Switching Between Pages

In the previous section, we’ve made the pages dynamic, but so far, just a single form page still displays. Let’s change our form pages by clicking the Next and Previous buttons. To achieve this, update the componentList array.

const componentList = [
  <SignUp
    page={page}
    setPage={setPage}
  />,
  <PersonalInfo
    page={page}
    setPage={setPage}
  />,
  <LocationInfo
    page={page}
    setPage={setPage}
  />, 
  <OtherInfo
    page={page}
    setPage={setPage}
  />,
];

In the code block above, we’re passing the page and the setPage into all components as props, enabling us to control the state of each component. Next, Let’s accept the props and manipulate the state in each component. Head over to the src/components/SignUp.jsx file and update the code.

const SignUp = ({page, setPage}) => {
  return ( 
      ...

      <button
          onClick={() => {
            setPage(page + 1);
          }}>
          Next
        </button>
    </div>
  );
};

In the code block above, we’re retrieving the page and setPage props, incrementing by 1 the page prop upon clicking the Next button. We’ll be doing the same to all the other components. Head over to src/components/PersonalInfo.jsx file and update the code.

const PersonalInfo = ({page, setPage}) => {
  return ( 
      ...

      <button
        onClick={() => {
          setPage(page + 1);
        }}>
        Next
      </button>
      <br/>
      <button
        onClick={() => {
          setPage(page - 1);
        }}>
        Previous
      </button>
    </div>
  );
};

export default PersonalInfo

In the code block above, we’re updating the state by incrementing by 1 in the Next and decrementing it by 1 in the Previous button. Head over to the src/components/LocationInfo.jsx file and update the code.

const LocationInfo = ({page, setPage}) => {
  return ( 
      ...

      <button
        onClick={() => {
          setPage(page + 1);
        }}>
        Next
      </button>
      <br />
      <button
        onClick={() => {
          setPage(page - 1); 
        }}>
        Previous
        </button>
    </div>
  );
};

Like we did in the previous code block, we’re updating the state by incrementing by 1 in the Next and decrementing it by 1 in the Previous button. Finally, head over to the src/components/OtherInfo.jsx file and update the code.

const OtherInfo = ({ page, setPage }) => {
  return (    
      ...

      <button
        onClick={() => {
          alert("You've successfully submitted this form");
        }}>
        Submit
      </button>
      <br />
      <button
        onClick={() => {
          setPage(page - 1);
        }}>
        Previous
      </button>
    </div>
  );
};

In the code block above, we’re displaying an alert dialog signifying the end of the form pages. We’re also decreasing the state by 1 to return to the previous form in the Previous button. Upon checking our process, you should have the results below.

2

Preserving entered data across steps

In this section, we’ll be saving our values in our text field when switching pages to allow users to modify their previous data. Head to the App.js and paste the code below into the App() component.

function App() {
  const [formData, setFormData] = useState({
    fullname: "",
    username: "",
    password: "",
    nickname: "",
    email: "",
    address: "",
    nationality: "",
    zipcode: "",
    highestQualification: "", 
    occupation: "",
    about: "",
  });

...
}

In the above code, we created a formData hook and set its initial value to the object we want to update from our text field. Let’s update each component in the componentList.

const componentList = [
  <SignUp
    formData={formData}
    setFormData={setFormData}
    page={page}
    setPage={setPage}
  />,
  <PersonalInfo
    formData={formData}
    setFormData={setFormData}
    page={page}
    setPage={setPage}
  />,
  <LocationInfo
    formData={formData}
    setFormData={setFormData}
    page={page}
    setPage={setPage}
  />,
  <OtherInfo
    formData={formData}
    setFormData={setFormData}
    page={page}
    setPage={setPage}
  />,
];

In the above code, we are passing the formData and the setFormData as props into each component in the componentList. Let’s grab the formData value and update the setFormData with our form input. Head over src/components/SignUp.jsx file and update the component.

const SignUp = ({ page, setPage, formData, setFormData }) => {  // grabbing the props
  return (
      <div>
        <input
          type="text"
          placeholder="Full Name"
          value={formData.fullName} //setting the value of the form to the props value
          onChange={(e) =>
            setFormData({ ...formData, fullName: e.target.value })  //setting the formData to the value input of the textfield 
          }
        />
        <input
          type="text"
          placeholder="Username"
          value={formData.username}
          onChange={(e) =>
            setFormData({ ...formData, username: e.target.value }) 
          }
        />
        <input
          type="text"
          placeholder="Password"
          value={formData.password}
          onChange={(e) =>
            setFormData({ ...formData, password: e.target.value })
          }
        />

      ...
      </div>
  );
};

In the above code, we’re grabbing the formData and the setFormData function as props, updating the text field’s value to the formData value and updating the object in the formData to our typed value. We’ll do the same for all the other components. Head over src/components/PersonalInfo.jsx file and update the component.

const PersonalInfo = ({ page, setPage, formData, setFormData }) => {
  return (
      <div>
        <input
          type="text"
          placeholder="Nickname"
          value={formData.nickname}
          onChange={(e) => setFormData({ ...formData, nickname: e.target.value })}
        />
        <input
          type="text"
          placeholder="Email"
          value={formData.email}
          onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        />

      ...
      </div> 
  );
};

Head over src/components/LocationInfo.jsx file and update the component.

const LocationInfo = ({ page, setPage, formData, setFormData }) => {
  return (
      <div>
        <input
          type="text"
          placeholder="Address"
          value={formData.address}
          onChange={(e) => setFormData({ ...formData, address: e.target.value })}
        />
        <input
          type="text"
          placeholder="Nationality"
          value={formData.nationality}
          onChange={(e) => setFormData({ ...formData, nationality: e.target.value })}
        />
        <input
          type="text"
          placeholder="Zipcode"
          value={formData.zipcode}
          onChange={(e) => setFormData({ ...formData, zipcode: e.target.value })}
        />

      ...
      </div>
  );
};

In the code above, we’re updating the value and the onchanged of the address, nationality, and zipcode. Finally, Head over src/components/OtherInfo.jsx file and update the component.

const OtherInfo = ({ page, setPage, formData, setFormData }) => {
  return (
      <div>
        <input
          type="text"
          placeholder="Occupation" 
          value={formData.occupation}
          onChange={(e) =>
            setFormData({ ...formData, occupation: e.target.value })
          }
        />
        <textarea
          type="text"
          placeholder="About"
          value={formData.about}
          onChange={(e) => setFormData({ ...formData, about: e.target.value })}
        />

      ...
      </div>
  );
};

Here, we’re also updating the value and setting the formData to our typed input. With these done, we’ll have a form looking like this.

3

Create a progress display header

Using a progress bar, let’s show users the current page they’re on. Head to the App.js file and update the progress-bar div.

return (
  <div className="App">
    <div className="progress-bar">
      <div style={{width: page === 0? "25%": page === 1? "50%": page === 2? "75%" : "100%"}}></div>
    </div> 

  ...
  </div>
);

In the code block above, we’re styling the progress bar by increasing the indicator’s width in the div tag according to the page index. Thus, the width will change whenever there is an update to the page state.

4

Adding a transition effect to the form

Let’s give our form some swift animation when navigating to and fro. This section will cover how to add nice animations when navigating to and from the different forms. For this, we will be installing the Framer motion package. Run the command below to install it

npm install framer-motion 
// OR
yarn add framer-motion

We’ll be adding a transition effect to all our form components, so make sure to import it into the various form components (SignUp, PersonalInfo, LocationInfo, and OtherInfo).

import { motion } from "framer-motion";

Now add the x prop and update the div tag to all the components listed above.

const SignUp = ({ formData, setFormData, x}) => { //added x
  return (
      <motion.div                            //updated the div tag
          initial={{ x: x }}
          transition={{ duration: 1 }}
          animate={{ x: 0 }}
        >

      ...
      </motion.div>
    );
}

In the above code, we have added an x parameter to our prop and updated our div tag to motion.div. Then we set our animation to the values from our prop. Note: Ensure to make the above update to the PersonalInfo, LocationInfo, and OtherInfo Component.

Finally, head to the App.js file and add the hook below.

const [x, setX] = useState(0);

The hook above is added to the App() function. We’ll be using this to control our transition flow. Next, pass the hook into every component in the componentList.

const componentList = [
  <SignUp 
    formData={formData}
    setFormData={setFormData}
    page={page}
    setPage={setPage}
    x={x}
    setX={setX}
  />,
  <PersonalInfo
    formData={formData}
    setFormData={setFormData}
    page={page}
    setPage={setPage}
    x={x}
    setX={setX}
  />,
  <LocationInfo
    formData={formData}
    setFormData={setFormData}
    page={page}
    setPage={setPage}
    x={x}
    setX={setX}
  />,
  <OtherInfo
    formData={formData}
    setFormData={setFormData}
    page={page}
    setPage={setPage}
    x={x}
    setX={setX}
  />,
];

Then, control the animation flow on navigation between pages. In the Previous buttons, add the update to the x state. Head over to the PersonalInfo, LocationInfo, and OtherInfo and update the Previous button to the code below.

<button
  onClick={() => {
    setPage(page - 1);
    setX(-1000); //here, we're manipulating the xstate
  }}
>
  Previous
</button>

In the code block above, we’re setting the value of our x state to -1000, making the animation start from left to right. Update the code in the Next button by updating the x state in the SignUp, PersonalInfo, and LocationInfo.

<button onClick={() => {setPage(page + 1); setX(1000);}}>
  Next
</button>

In the above code, we’re updating the x state to 1000 to give the form a right-to-left transition. With all this done, we should be able to achieve a dynamic form with cool transitions.

5

Conclusion

In this tutorial, we’ve been able to create dynamic forms, preserve form states, form progress bar and add transitions to our form.

The entire source code can be found on GitHub.