DEV Community

Cover image for Creating a fancy stepper component in React
PixoDev
PixoDev

Posted on • Originally published at pixo.sh

Creating a fancy stepper component in React

This is an Stepper made in React:

full_stepper-2

Steppers let you display content in sequential steps, they are good for:

  • dynamically split big forms, so the user doesn’t need to fill 20000 inputs at once
  • logically present data, for example, to understand the content in step 2 you need to see step 1 first

In this tutorial we are going to create a Stepper component in React, taking care of the usability, and we are going to create a cool and fancy one.

If you don’t want to go through the tutorial, the code is in Github already, here

Create the project

First things first, let’s create our project, we are going to create one with ‘create-react-app’ and Typescript

npx create-react-app fancy-stepper --template typescript

Once our project is generated:
cd fancy-stepper && yarn start

Your app should be running on localhost:3000

Preparing the component

The next step is to create our Stepper component. Let’s go to our src directory and let’s create a file called Stepper.tsx, and we are going to create our component like this:

import React from 'react';

interface StepperProps {
    // Empty right now, we will fill this in later
}

export const Stepper: React.FC<StepperProps> = () => {
    return <>Nothing yet</>
}
Enter fullscreen mode Exit fullscreen mode

Now, go to your App.tsx file, and remove everything, then add your Stepper component.

import React from 'react';
import { Stepper } from './Stepper';
import './App.css';

function App() {
  return (
    <div>
      <Stepper />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Creating our Stepper functionalities

If we make a breakdown of what a Stepper can do, we can summarize it like this:

  • Display N steps
  • Go to next step
  • Go to the previous step
  • Update the Stepper progress

The Steps

We are going to pass steps to the stepper component using the render props pattern, let’s start creating a steps prop in our component. That prop will accept an array of objects, and each object will configure each step, let’s write our types first:

import React from "react";

interface StepperProps {
    steps: Step[];
}

interface Step {
    // Title of the step
    title: string;
    // Element to render in the step, can contain
    // a form, an image, whatever
    element: (stepProps:StepProps) => JSX.Element;
}

export interface StepProps {
  // Here we tell the stepper to go to the next or previous step from
  // the element we are rendering
  goNextStep: () => void;
  goPreviousStep: () => void;
  // Tells you the active step right now
  currentStep: number;
  // And this is useful to know where you are
  isLast: boolean;
  isFirst: boolean;
  // Tells you the step in which you are right now, starting
  // from 1
  step: number;
}

export const Stepper: React.FC<StepperProps> = ({steps}) => {
  return <>Nothing yet</>;
};
Enter fullscreen mode Exit fullscreen mode

You will notice, now in our App.tsx file, we have an error because the Stepper component is missing the steps prop, let’s add it:

import React from "react";
import { Stepper } from "./Stepper";
import "./App.css";

function App() {
  return (
    <div>
      <Stepper
        steps={[
          {
            title: "I'm the step 1",
            // Render whatever you want here, we will improve this later
            element: ({ goNextStep, goPreviousStep }) => <>Step 1</>,
          },
          {
            title: "I'm the step 2",
            element: ({ goNextStep, goPreviousStep }) => <>Step 2</>,
          },
        ]}
      />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Nice!, now our steps and our Stepper are ready.

Rendering our Steps

We need to display the steps sequentially since we don’t want the steps to appear and disappear from the DOM, because that’s not good for accessibility, we are going to render them linearly with an overflow: hidden wrapper, like this:

Alt Text

The red border represents the visible area, and each grey box represents each step, we only see the step which is currently inside the red area.

Let’s start by rendering the steps in our Stepper component:

export const Stepper: React.FC<StepperProps> = ({ steps }) => {
  const goNextStep = () => {};
  const goPreviousStep = () => {};

  return (
    <div className="stepper stepper-wrapper">
      {/* This div represents the red bordered box */ }
      <div className="stepper-selector">
        {steps.map(step => (
          <div>
            <step.element
              // NOOP right now, we will update this later
              goNextStep={goNextStep}
              goPreviousStep={goPreviousStep}
              // Fill this with fake values, we will go
              // over this later
              currentStep={0}
              isFirst={false}
              isLast={false}
            />
          </div>
        ))}
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now, state

Our stepper needs to store the value of the active step, we are going to use React state for this, how we are going to update this is using the goNextStep and goPreviousStep functions, those functions are being passed to the steps we are rendering.

export const Stepper: React.FC<StepperProps> = ({ steps }) => {
  const [currentStep, setCurrentStep] = useState<number>(1);
  const goNextStep = () => {
    const nextStep = currentStep + 1;
    if (nextStep <= steps.length) {
      setCurrentStep(nextStep);
    }
  };

  const goPreviousStep = () => {
    const previousStep = currentStep - 1;
    if (previousStep >= 1) {
      setCurrentStep(previousStep);
    }
  };

  return (
    <div className="stepper stepper-wrapper">
      <div className="stepper-selector">
        {steps.map((step, i) => (
          <div className="step-wrapper">
            <step.element
              step={i + 1}
              goNextStep={goNextStep}
              goPreviousStep={goPreviousStep}
              // From our state
              currentStep={currentStep}
              // Check if this step is the first one
              isFirst={i === 0}
              // Check if its the last one
              isLast={i === steps.length - 1}
            />
          </div>
        ))}
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Making it fancy

Now let’s improve what we render in each step, so we can play with it a bit, we are going to add transitions too.

function App() {
  return (
    <div className="wrapper">
      <Stepper
        steps={[
          {
            title: "I'm the step 1",
            // Render whatever you want here, we will improve this later
            element: stepProps => <Step {...stepProps} />,
          },
          {
            title: "I'm the step 2",
            element: stepProps => <Step {...stepProps} />,
          },
        ]}
      />
    </div>
  );
}

export default App;

const Step: React.FC<StepProps> = ({
  goNextStep,
  goPreviousStep,
  isFirst,
  isLast,
  currentStep,
  step,
}) => {
  return (
    <div className="step">
      <div className="step-body">IM THE STEP {step}</div>
      <div className="step-actions">
        {/* If we are in the Step 1, we cannot go back, so we disable this */}
        <button
          className="step-button"
          disabled={isFirst}
          onClick={goPreviousStep}
        >
          GO PREVIOUS
        </button>
        {/* Same but with the last step */}
        <button className="step-button" disabled={isLast} onClick={goNextStep}>
          GO NEXT
        </button>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

If you go to your browser, you will see an ugly HTML layout, so we are going to add some styles to improve that:

/* App.css */
.step {
  height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background: #fff;
}

.step-body {
  flex: 1;
  justify-content: center;
  align-items: center;
  display: flex;
}

.step-actions {
  display: inline-flex;
  justify-content: space-between;
  margin: 0 2rem 1rem;
}

.step-button {
  padding: 0.5rem 1rem;
  border: none;
}
Enter fullscreen mode Exit fullscreen mode
/* Stepper.css */

.stepper {
  width: 600px;
  height: 600px;
  position: relative;
  overflow: hidden;
  display: inline-block;
  box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 1px -2px,
    rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px;
}

.step-wrapper {
  width: 600px;
  height: 100%;
}

.stepper-selector {
  position: absolute;
  height: 100%;
  display: inline-flex;
    top:0;
}
Enter fullscreen mode Exit fullscreen mode

And now, let’s add our functionality to switch between steps, we are going to use a ref for this.

export const Stepper: React.FC<StepperProps> = ({ steps }) => {
  const [currentStep, setCurrentStep] = useState<number>(1);
  const stepperSelector = useRef<HTMLDivElement>(null);
  // Every time our currentStep is updated, we are going to trigger this
  useEffect(() => {
    moveStepper();
  }, [currentStep]);

  const goNextStep = () => {
    const nextStep = currentStep + 1;
    if (nextStep <= steps.length) {
      setCurrentStep(nextStep);
    }
  };

  const goPreviousStep = () => {
    const previousStep = currentStep - 1;
    if (previousStep >= 1) {
      setCurrentStep(previousStep);
    }
  };

  const moveStepper = () => {
    if (stepperSelector.current) {
      const stepper = stepperSelector.current;
      const stepWidth = stepper.offsetWidth / steps.length;
      stepper.style.transform = `translateX(-${
        stepWidth * (currentStep - 1)
      }px)`;
    }
  };

  return (
    <div className="stepper stepper-wrapper">
      {/* This will display our current step */}
      <div className="stepper-selector" ref={stepperSelector}>
        {steps.map((step, i) => (
          <div className="step-wrapper">
            <step.element
              step={i + 1}
              goNextStep={goNextStep}
              goPreviousStep={goPreviousStep}
              // From our state
              currentStep={currentStep}
              // Check if this step is the first one
              isFirst={i === 0}
              // Check if its the last one
              isLast={i === steps.length - 1}
            />
          </div>
        ))}
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Here we are getting a ref of the DOM element which contains the steps, we are going to move it every time we update the stepper.

Adding a progress bar to the stepper

Time to add a progress bar, so we know where we are in the stepper.

Let’s create a new component in a file called StepperProgress.tsx, it should look like this:

import React from "react";
import "./Stepper.css";

interface StepperProgressProps {
  stepTitles: string[];
  currentStep: number;
}
export const StepperProgress: React.FC<StepperProgressProps> = ({
  stepTitles,
  currentStep,
}) => {
    // Calculate the progress for each step we fill
  const progressPerStep = 100 / (stepTitles.length - 1);
    // Calculate the progress based on the step we are in
  const progress = (currentStep - 1) * progressPerStep;
  return (
    <div className="stepper-progress">
      <div className="stepper-progress-wrapper">
        <div
          className="stepper-progress-bar"
          style={{ width: progress + "%" }}
        />
        {stepTitles.map((title, i) => (
          <div className="step-title">
            <div className="step-title-number">{i + 1}</div>
            {title}
          </div>
        ))}
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

This component will display a progress bar and will update the progress bar width every time we update the current step.

In our Stepper.tsx file let’s call the component:

// Rest of the Stepper.tsx file

return <div className="stepper stepper-wrapper">
      <StepperProgress
        stepTitles={steps.map(step => step.title)}
        currentStep={currentStep}
      />
      {/* This will display our current step */}
      <div className="stepper-selector" ref={stepperSelector}>
        {steps.map((step, i) => (
          <div className="step-wrapper">
            <step.element
              step={i + 1}
              goNextStep={goNextStep}
              goPreviousStep={goPreviousStep}
              // From our state
              currentStep={currentStep}
              // Check if this step is the first one
              isFirst={i === 0}
              // Check if its the last one
              isLast={i === steps.length - 1}
            />
          </div>
        ))}
      </div>
    </div>
Enter fullscreen mode Exit fullscreen mode

And now let’s add some CSS for this:

// Stepper.css

// Rest of the CSS file
.stepper-progress {
  position: absolute;
  top: 15px;
  width: 100%;
  z-index: 9;
}

.stepper-progress-wrapper {
  width: 90%;
  position: relative;
  display: flex;
  margin: auto;
  justify-content: space-between;
}

.step-title {
  text-align: center;
  font-size: 0.7rem;
  align-items: center;
  background: #fff;
  padding: 0 1rem;
  height: 30px;
}

.step-title-number {
  font-size: 1rem;
  background: #ceeeff;
  height: 24px;
  width: 24px;
  margin: auto;
  line-height: 1.5;
  border: 3px solid #fff;
  border-radius: 100%;
}

.stepper-progress-bar {
  position: absolute;
  width: 100%;
  height: 3px;
  top: 13px;
  z-index: -1;
  background: #e91e63;
  transition: width 1s cubic-bezier(0.23, 1, 0.32, 1) 0s;
}
Enter fullscreen mode Exit fullscreen mode

The result:

full_stepper-2

Top comments (0)