Nowadays, almost all websites and applications have forms where we submit important details and send messages to the app handler. However, creating multi-step forms is considered very effective for user experience when user input has to be handled in many steps, such as in registration processes, surveys, and checkout forms.
In this article, we will teach you how to create a multi-step form with a progress bar using Tailwind CSS and ReactJS. We will describe the project step by step in easy language so that you will be able to create this project completely and also understand it.
So let's get started.
Table of contents:
- Prerequisites
- Project Setup with Tailwind CSS Configuration
- Starting project
- Building logic inside components
- Enhancing project
Prerequisites
Before we start developing a multi-step form with a progress bar using Tailwind CSS and ReactJS, ensure you have the following prerequisites:
- Basic Knowledge of ReactJS and React Hook.
- Basic Knowledge of ReactJS Context API.
- Basic Understanding of Tailwind CSS.
- Node.js and npm Installed.
I hope you have all the prerequisites mentioned above. Now, let’s start with the project and see how we can build it step by step.
Project Setup with Tailwind CSS Configuration
First, set up a new React project if you don't have one already:
npx create-react-app multi-step-form
cd multi-step-form
Once you've used this command, your project is ready. Now, let's organize our folder structure. We'll remove unnecessary files and clean up some code in certain files. While we're removing these files, it doesn't mean they're unnecessary—they have their advantages. However, for this project, we don't need them, so we're removing them. We're organizing everything clearly because it's important in professional development.
In the "public" folder, keep only the index.html file and delete the rest. In the "src" folder, keep App.css, App.js, index.css, and index.js, and delete the other files.
Now let's delete all the code from the App.css, index.css, index.js, and App.js files and add the following code inside the App.js and index.js files.
// src\App.js
import React from 'react'
function App() {
return (
<div>App</div>
)
}
export default App;
// src\index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
//
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
Now your entire project should look like the image below.
Great! Our React project is ready. Now we need to install Tailwind CSS and configure it in this project. Let's do this together.
Install Tailwind CSS:
npm install -D tailwindcss //This command will install Tailwind CSS in your project
Initialize Tailwind CSS:
npx tailwindcss init
After running the above command, it will automatically generate one file named "tailwind.config.js" in your project.
Update tailwind.config.js file:
In the "tailwind.config.js" file, add the following code.
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
}
Update index.css file:
In the "index.css" file, include the following code.
/* src\index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Congratulations! Our project setup is ready. Now we can use Tailwind CSS. Let's start building the project!
Now, if you create the following files and copy-paste the code, it will work. But you might not fully understand it. I recommend you read carefully and paste the code into your project to understand how it works.
Starting project
For this project, we need to create some folders and files. First, create a "components" folder inside the "src" directory. Then, create two files inside the "components" folder: "Stepper.jsx" and "StepperControl.jsx".
Next, create a folder named "steps" inside the "components" folder. Then, inside the "steps" folder, create four files: Account.jsx, Details.jsx, Final.jsx, and Payment.jsx.
Next, create another folder inside the "src" directory named "contexts". Inside the "contexts" folder, create one file named "StepperContext.js".
Now your project folder structure should look like this:
Building logic inside components
In the StepperControl.jsx file, we will create "Back" and "Next" buttons that handle navigation between steps when clicked.
// src\components\StepperControl.jsx
function StepperControl({ handleClick, currentStep, steps }) {
return (
<div className='container flex justify-around mt-4 mb-8'>
{/* back button */}
<button
onClick={() => handleClick()}
className={`bg-white text-slate-400 uppercase py-2 px-4 rounded-xl font-semibold border-2 border-slate-300 hover:bg-slate-700 hover:text-white transition duration-200 ease-in-out ${currentStep === 1 ? "opacity-50 cursor-not-allowed" : " "}`}>Back</button>
{/* Next button */}
<button
onClick={() => handleClick("next")}
className='bg-green-500 text-white uppercase py-2 px-4 rounded-xl font-semibold cursor-pointer hover:bg-slate-700 hover:text-white transition duration-200 ease-in-out'>{currentStep === steps.length - 1 ? "Confirm" : "Next"}</button>
</div>
)
}
export default StepperControl
In the StepperControl component, the "Back" button is disabled on the first step, and the "Next" button changes to "Confirm" on the last step. These buttons use the handleClick function to move between steps.
In the Stepper.jsx file we will create a component that displays the steps of a multi-step form.
// src/components/Stepper.jsx
import { useEffect, useRef, useState } from 'react';
function Stepper({ steps, currentStep }) {
const [newStep, setNewStep] = useState([]);
const stepRef = useRef();
useEffect(() => {
// Function to update steps based on the current step number
const updateStep = (stepNumber, steps) => {
const newSteps = [...steps];
let count = 0;
while (count < newSteps.length) {
// Current step
if (count === stepNumber) {
newSteps[count] = {
...newSteps[count],
highlighted: true,
selected: true,
completed: true,
};
count++;
}
// Step completed
else if (count < stepNumber) {
newSteps[count] = {
...newSteps[count],
highlighted: false,
selected: true,
completed: true,
};
count++;
}
// Step pending
else {
newSteps[count] = {
...newSteps[count],
highlighted: false,
selected: false,
completed: false,
};
count++;
}
}
return newSteps;
};
// Create initial steps state
const stepsState = steps.map((step, index) =>
Object.assign(
{},
{
description: step,
completed: false,
highlighted: index === 0 ? true : false,
selected: index === 0 ? true : false,
}
)
);
stepRef.current = stepsState;
const current = updateStep(currentStep - 1, stepRef.current);
setNewStep(current);
}, [steps, currentStep]);
// Function to display each step
const displaySteps = newStep.map((step, index) => {
return (
<div
key={index}
className={index !== newStep.length - 1 ? 'w-full flex items-center' : 'flex items-center'}>
<div className="relative flex flex-col items-center text-teal-600">
<div className={`rounded-full transition duration-500 ease-in-out border-2 border-gray-300 h-12 w-12 flex items-center justify-center py-3 ${step.selected ? "bg-green-600 text-white font-bold border border-green-600" : " "}`}>
{/* Display step number or checkmark if completed */}
{step.completed ? (
<span className='text-white font-bold text-xl'>✓</span>
) : (index + 1)}
</div>
<div className={`absolute top-0 text-center mt-16 w-50 text-xs font-medium uppercase ${step.highlighted ? "text-gray-900" : "text-gray-400"}`}>
{/* Display step description */}
{step.description}
</div>
</div>
<div className={`flex-auto border-t-2 transition duration-500 ease-in-out ${step.completed ? "border-green-600" : "border-gray-300"}`}></div>
</div>
);
});
return (
<div className="mx-4 p-4 flex justify-between items-center">
{displaySteps}
</div>
);
}
export default Stepper;
In the Stepper.jsx file, we've created a Stepper component to manage and display the progress of steps in a multi-step form. This component dynamically updates each step's appearance based on the current step and overall progress. Using state management, it highlights the current step, shows completed steps with a checkmark, and adjusts the visual state of pending steps. By initializing the steps and determining the active step using currentStep, the component dynamically renders either step numbers or checkmarks, ensuring a clear and intuitive user experience.
In the StepperContext.js file, we will create a context to manage and share user data across multiple steps in a multi-step form.
// src/contexts/StepperContext.js
import { createContext, useState } from 'react';
export const StepperContext = createContext();
export const StepperProvider = ({ children }) => {
const [userData, setUserData] = useState({});
return (
<StepperContext.Provider value={{ userData, setUserData }}>
{children}
</StepperContext.Provider>
);
};
In StepperContext.js, we've created a context using React's createContext. This context manages user data for our multi-step form. We set up a StepperProvider component that uses useState to keep track of userData, which stores the form data, and setUserData, which updates this data. StepperProvider wraps its children with StepperContext.Provider. This allows any component within the context to easily access and modify the user data.
In the Account.jsx file, we create a form for username and password, updating the userData state through the handleChange function.
// src/components/steps/Account.jsx
import { useContext } from 'react';
import { StepperContext } from '../../contexts/StepperContext';
function Account() {
const { userData, setUserData } = useContext(StepperContext);
const handleChange = (e) => {
const { name, value } = e.target;
setUserData({ ...userData, [name]: value });
};
return (
<div className='flex flex-col'>
<div className='w-full mx-2'>
<div className='font-bold h-4 text-gray-500 text-xs'>
USERNAME
</div>
<div className='my-2 p-1 border border-gray-200 rounded'>
<input
onChange={handleChange}
value={userData["username"] || ""}
name='username'
placeholder='username'
className='p-1 px-2 outline-none w-full text-gray-800'
/>
</div>
</div>
<div className='w-full mx-2'>
<div className='font-bold h-4 mt-3 text-gray-500 text-xs'>
PASSWORD
</div>
<div className='my-2 p-1 border border-gray-200 rounded'>
<input
onChange={handleChange}
value={userData["password"] || ""}
name='password'
placeholder='password'
type='password'
className='p-1 px-2 outline-none w-full text-gray-800'
/>
</div>
</div>
</div>
);
}
export default Account;
In Account.jsx, we created a React component that utilizes useContext to access userData and setUserData from the StepperContext. This component manages user input for username and password fields within a multi-step form. The handleChange function updates userData based on user input, ensuring seamless data management across the form's steps.
In the Details.jsx file, we create a form for address and city, updating the userData state through the handleChange function.
// src/components/steps/Details.jsx
import { useContext } from 'react';
import { StepperContext } from '../../contexts/StepperContext';
function Details() {
const { userData, setUserData } = useContext(StepperContext);
// Function to handle input changes and update userData state
const handleChange = (e) => {
const { name, value } = e.target;
setUserData({ ...userData, [name]: value });
};
return (
<div className='flex flex-col'>
{/* Address input */}
<div className='w-full mx-2'>
<div className='font-bold h-4 text-gray-500 text-xs'>
ADDRESS
</div>
<div className='my-2 p-1 border border-gray-200 rounded'>
<input
onChange={handleChange}
value={userData["address"] || ""}
name='address'
placeholder='Address'
className='p-1 px-2 outline-none w-full text-gray-800'
/>
</div>
</div>
{/* City input */}
<div className='w-full mx-2'>
<div className='font-bold h-4 mt-3 text-gray-500 text-xs'>
CITY
</div>
<div className='my-2 p-1 border border-gray-200 rounded'>
<input
onChange={handleChange}
value={userData["city"] || ""}
name='city'
placeholder='City'
className='p-1 px-2 outline-none w-full text-gray-800'
/>
</div>
</div>
</div>
);
}
export default Details;
In Details.jsx, we created a component that uses useContext to access userData and setUserData from StepperContext. This component handles user input for the address and city fields. When users type in these fields, the handleChange function updates the userData state in real-time, ensuring the data stays consistent throughout the form.
In the Payment.jsx file, we create a form for credit card and expiration date, updating the userData state through the handleChange function.
// src/components/steps/Payment.jsx
import { useContext } from 'react';
import { StepperContext } from '../../contexts/StepperContext';
function Payment() {
const { userData, setUserData } = useContext(StepperContext);
// Function to handle input changes and update userData state
const handleChange = (e) => {
const { name, value } = e.target;
setUserData({ ...userData, [name]: value });
};
return (
<div className='flex flex-col'>
{/* Credit Card input */}
<div className='w-full mx-2'>
<div className='font-bold h-4 text-gray-500 text-xs'>
CREDIT CARD
</div>
<div className='my-2 p-1 border border-gray-200 rounded'>
<input
onChange={handleChange}
value={userData["creditCard"] || ""}
name='creditCard'
placeholder='1234 5678 9012 3456'
className='p-1 px-2 outline-none w-full text-gray-800'
/>
</div>
</div>
{/* Expiry Date input */}
<div className='w-full mx-2'>
<div className='font-bold h-4 mt-3 text-gray-500 text-xs'>
EXP
</div>
<div className='my-2 p-1 border border-gray-200 rounded'>
<input
onChange={handleChange}
value={userData["exp"] || ""}
name='exp'
placeholder='YY/MM/DD'
className='p-1 px-2 outline-none w-full text-gray-800'
/>
</div>
</div>
</div>
);
}
export default Payment;
In Payment.jsx, we created a component using useContext to access userData and setUserData from StepperContext. This component handles user input for credit card and expiry date fields. The handleChange function updates the userData state as users type, keeping the payment details consistent across the form.
In Final.jsx file, we'll create a React component to display a congratulatory message and an icon when the multi-step form is completed. We will include an SVG icon, a message confirming account creation, and a button to close the form and go back to the homepage.
// src/components/steps/Final.jsx
function Final() {
return (
<div className="flex flex-col items-center">
<div>
<svg
className='w-24' fill='#16a34a' viewBox='0 0 20 20' xmlns="http://www.w3.org/2000/svg">
<path
fillRule='evenodd'
d='M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z'
/>
</svg>
</div>
<div className="mt-5 text-xl font-semibold text-green-500"> 🎉 CONGRATULATIONS! 🎉</div>
<div className="text-lg font-semibold text-gray-400"> Your Account has been created. </div>
<a className='mt-10' href="/">
<button className='h-10 px-10 text-green-700 transition-colors duration-150 border border-gray-300 rounded-lg hover:bg-green-500 hover:text-white'>Close</button>
</a>
</div>
);
}
export default Final;
In Final.jsx, we created a React component to celebrate finishing a multi-step form. This component includes an SVG icon, a text message congratulating the user, and another message confirming their account creation. We've added a close button that automatically redirects users to the homepage when clicked.
In the App.js file, we'll create the main component for a multi-step form. It manages the current step and user data, allowing smooth navigation between form steps.
// src\App.js
import { useState } from 'react';
import { StepperContext } from './contexts/StepperContext';
import Stepper from './components/Stepper';
import StepperControl from './components/StepperControl';
import Account from './components/steps/Account';
import Details from './components/steps/Details';
import Final from './components/steps/Final';
import Payment from './components/steps/Payment';
function App() {
// State to keep track of the current step
const [currentStep, setCurrentStep] = useState(1);
// State to hold user data collected from the form steps
const [userData, setUserData] = useState('');
// State to hold final data after completion
const [finalData, setFinalData] = useState([]);
// Steps for the multi-step form
const steps = ['Account information', 'Personal Details', 'Payment', 'Complete'];
// Function to display the current step component
const displayStep = (step) => {
switch (step) {
case 1:
return <Account />;
case 2:
return <Details />;
case 3:
return <Payment />;
case 4:
return <Final />;
default:
return null;
}
};
// Function to handle navigation between steps
const handleClick = (direction) => {
let newStep = currentStep;
direction === 'next' ? newStep++ : newStep--;
// Check if steps are within bounds
if (newStep > 0 && newStep <= steps.length) {
setCurrentStep(newStep);
}
};
return (
<div className="md:w-1/2 mx-auto shadow-xl rounded-2xl pb-2 bg-white">
<div className="container horizontal mt-5">
<Stepper steps={steps} currentStep={currentStep} />
<div className='my-10 p-10'>
<StepperContext.Provider value={{ userData, setUserData, finalData, setFinalData }}>
{displayStep(currentStep)}
</StepperContext.Provider>
</div>
</div>
{currentStep !== steps.length && (
<StepperControl
handleClick={handleClick}
currentStep={currentStep}
steps={steps}
/>
)}
</div>
);
}
export default App;
In App.js, we've built the main component for our multi-step form. Using useState, we handle the currentStep, userData, and finalData. The steps array defines each form step. Based on the current step, the displayStep function selects and displays the corresponding component (Account, Details, Payment, or Final). Navigation between steps is managed by handleClick, which adjusts the currentStep state. The StepperContext.Provider wraps the step components, providing access to user and final data context. Additionally, Stepper and StepperControl components aid in step navigation and control.
Congratulations! If you have followed this article and the code correctly, you should have successfully created this project without any errors, and your UI should resemble the following GIF.
Enhancing project
You can make this project more attractive which will enhance the user experience. For example, if a user tries to go to the next step without filling in the required fields, show them an error message to prevent them from moving forward without entering the needed information.
You can also add real-time feedback where the user can get an instant verification message as soon as they type any particular information.
Another enhancement can be saving the form state so users can resume their progress later if they navigate away from the page. Also if you have multiple forms and some sections are optional, you can add a skip button feature, allowing users to skip those parts and complete the form submission more efficiently.
Conclusion
We quickly learned how to create a multi-step form with a progress bar in React JS using Tailwind CSS. In an era where data is king, this forms a potent method of collecting it. We have seen how in the front-end part we can create good UX by clear navigation, and ensure that the process is smoothly transitioning from one step to another, it will not only give a better user experience but also make the entire project more efficient. It does not matter if you want to create a designed registration form or submit data on thousands of different interactions, these best practices are probably going to make your web developer's career easier.
If you have any questions about this article or related to web development, you can ask them in the question box given below, and you will get the answer soon.