The Ultimate MUI Stepper Example: Color, Forms, & More

The Material UI Stepper component is a great way to direct users to fill out forms or complete certain tasks. In this tutorial we will change the background color of the stepper, change step button active and completed color, and insert a custom icon into a step.

Here’s what we will build:

MUI Stepper with Custom Color and Buttons
MUI Stepper with Custom Color and Buttons

I started with this great example in the MUI docs and customized it. Full code for this demo is in the Resources section.

MUI Stepper Background Color and Box Shadow

The Material UI Stepper is composed of a Stepper component and a variety of subcomponents. In my example I am using Step and StepButton.

Here’s how the DOM renders with these components. Notice how deeply nested this component is:

MUI Stepper DOM
MUI Stepper DOM

We can change the Stepper background color, box shadow, and other stylings by adding styling values to the Stepper sx prop:

<Stepper nonLinear activeStep={activeStep} sx={ boxShadow: 2, backgroundColor: "rgba(0,0,0,0.1)", padding: 2 }>

This adds the background color and box shadow we see in the above screenshot.

MUI Stepper Buttons: Color and Icons

The most important part of styling the StepButton is knowing that MUI applies classes based on the state of the button. The classes are .Mui-active and .Mui-completed.

Notice in the DOM that step 2 has .Mui-active applied to the svg element.

We can use this information to style our buttons and the lines in between, which also receive active and completed classes. I created the following style object and replace the sx values in the Stepper component with the style object.

const stepStyle = {
  boxShadow: 2,
  backgroundColor: "rgba(0,0,0,0.1)",
  padding: 2,
  "& .Mui-active": {
    "&.MuiStepIcon-root": {
      color: "warning.main",
      fontSize: "2rem",
    },
    "& .MuiStepConnector-line": {
      borderColor: "warning.main"
    }
  },
  "& .Mui-completed": {
    "&.MuiStepIcon-root": {
      color: "secondary.main",
      fontSize: "2rem",
    },
    "& .MuiStepConnector-line": {
      borderColor: "secondary.main"
    }
  }
}

Since I added the stepStyle object at the top level of my stepper, I need to use proper selectors to target the buttons and icons. Visually it looks like we target the buttons, but really it is the svg with class MuiStepIcon-root. The MuiStepConnector-line class exists on the line between the buttons.

Alternatively, we could have applied some of these values to the Step or StepButton component sx props and had less nesting.

MUI Stepper Form Example

Creating a Stepper with a form is not too hard. The form can exist below the Stepper and simply observe any state value changes that occur on user interaction with the Stepper.

If some steps still need to be finished, then I display my form. I created a custom ContactForm and it requires one prop: the current Stepper step:

<ContactForm step={activeStep} />

This activeStep value is a React state value that gets updated on step completion. This ContactForm line of code comes immediately after the closing tag of the Stepper.

Below is the code inside ContactForm. It is a typical form. The only unique thing is that it displays a different FormGroup based on which step is active:

//ContactForm.tsx

import { useState } from "react";
import { FormGroup, TextField, Paper, Typography } from "@mui/material";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { DesktopDatePicker } from "@mui/x-date-pickers";
import { FormValues } from "./FormData";

interface FormValues {
  name?: string;
  role?: string;
  startDate?: string;
  // skills?: string[];
  // preference?: string;
}

const paperInputsStyle = {
  "& .MuiOutlinedInput-root": {
    "& > fieldset": { border: "1px solid", borderColor: "primary.main" },
    "&:hover": {
      "& > fieldset": { borderColor: "primary.light" }
    }
  },
  "& .MuiFormLabel-root": {
    color: "primary.dark"
  }
}
const today = new Date();

export default function ContactForm(props: {
  step: number
}) {

  const [formValues, setFormValues] = useState<FormValues>({ name: "Your Name Here", role: "Role?", startDate: `${today.getMonth() + 1}/${today.getDate()}/${today.getFullYear()}` });

  const handleTextFieldChange = (
    event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const { name, value } = event.target;
    setFormValues({
      ...formValues,
      [name]: value
    })
  }

  const handleDatePickerChange = (
    value: string | null | undefined
  ) => {
    console.log(value);
    const startDate = value as unknown as { month: () => string, date: () => string, year: () => string };
    setFormValues({
      ...formValues,
      startDate: `${startDate.month() + 1}/${startDate.date()}/${startDate.year()}`
    })
  }

  return (
    <>
      <Paper sx={{
        ...paperInputsStyle,
        margin: { xs: 1, sm: 2 },
      }}
      >
        <form>
          <FormGroup sx={{ display: props.step === 0 ? "" : "none" }}>
            <TextField
              id="name"
              name="name"
              value={formValues.name}
              sx={{ m: 2, width: 300 }}
              onChange={handleTextFieldChange}
            />
            <TextField
              id="role"
              name="role"
              value={formValues.role}
              sx={{ m: 2, width: 300 }}
              onChange={handleTextFieldChange}
            />
          </FormGroup>
          <FormGroup sx={{ display: props.step === 1 ? "" : "none", m: 2 }}>
            <LocalizationProvider dateAdapter={AdapterDayjs}>
              <DesktopDatePicker
                onChange={handleDatePickerChange}
                value={formValues.startDate}
                label="Date"
                inputFormat="MM/DD/YYYY"
                views={["day"]}
                renderInput={(params) => {
                  return <TextField sx={{ m: 2, width: 300 }} {...params} />
                }}
              />
            </LocalizationProvider>
          </FormGroup>
          <FormGroup sx={{ display: props.step === 2 ? "" : "none", m: 2 }}>
            <Typography>{`Name: ${formValues.name}`}</Typography>
            <Typography>{`Name: ${formValues.role}`}</Typography>
            <Typography>{`Name: ${formValues.startDate}`}</Typography>
          </FormGroup>
        </form>
      </Paper>
    </>
  );
}

The second FormGroup is showing in the screenshot below:

MUI Stepper Form Example
MUI Stepper Form Example

Resources

Here is the full code for the FormStepper.tsx file. It references the ContactForm component in the previous section.

import * as React from 'react';
//import Form from "@mui/material/Form";
import Stack from '@mui/material/Stack';
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepButton from '@mui/material/StepButton';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import ContactForm from "./ContactForm";
import VisibilityIcon from '@mui/icons-material/Visibility';

const steps = ['About You', 'Start Date', 'Review'];

const stepStyle = {
  boxShadow: 2,
  backgroundColor: "rgba(0,0,0,0.1)",
  padding: 2,
  "& .Mui-active": {
    "&.MuiStepIcon-root": {
      color: "warning.main",
      fontSize: "2rem",
    },
    "& .MuiStepConnector-line": {
      borderColor: "warning.main"
    }
  },
  "& .Mui-completed": {
    "&.MuiStepIcon-root": {
      color: "secondary.main",
      fontSize: "2rem",
    },
    "& .MuiStepConnector-line": {
      borderColor: "secondary.main"
    }
  }
}


export default function FormStepper() {
  const [activeStep, setActiveStep] = React.useState(0);
  const [completed, setCompleted] = React.useState<{
    [k: number]: boolean;
  }>({});

  const totalSteps = () => {
    return steps.length;
  };

  const completedSteps = () => {
    return Object.keys(completed).length;
  };

  const isLastStep = () => {
    return activeStep === totalSteps() - 1;
  };

  const allStepsCompleted = () => {
    return completedSteps() === totalSteps();
  };

  const handleNext = () => {
    const newActiveStep =
      isLastStep() && !allStepsCompleted()
        ? // It's the last step, but not all steps have been completed,
        // find the first step that has been completed
        steps.findIndex((step, i) => !(i in completed))
        : activeStep + 1;
    setActiveStep(newActiveStep);
  };

  const handleBack = () => {
    setActiveStep((prevActiveStep) => prevActiveStep - 1);
  };

  const handleStep = (step: number) => () => {
    setActiveStep(step);
  };

  const handleComplete = () => {
    const newCompleted = completed;
    newCompleted[activeStep] = true;
    setCompleted(newCompleted);
    handleNext();
  };

  const handleReset = () => {
    setActiveStep(0);
    setCompleted({});
  };

  return (
    <Stack sx={{ width: '100%' }}>
      <Stepper nonLinear activeStep={activeStep} sx={stepStyle}>
        {steps.map((label, index) => (
          <Step key={label} completed={completed[index]}>
            {
              index < 2 ?
                (<StepButton onClick={handleStep(index)}>
                  {label}
                </StepButton>) :
                (<StepButton
                  sx={activeStep === 2 ? { "& .MuiSvgIcon-root": { color: "warning.main", fontSize: "2rem" } } : allStepsCompleted() ? { "& .MuiSvgIcon-root": { color: "secondary.main", fontSize: "2rem" } } : { color: "rgba(0, 0, 0, 0.38)" }}
                  icon={<VisibilityIcon />}
                  onClick={handleStep(index)}
                >
                  {label}
                </StepButton>)
            }

          </Step>
        ))}
      </Stepper>
      {allStepsCompleted() ? (
        <React.Fragment>
          <Typography sx={{ mt: 2, mb: 1 }}>
            All steps completed - you&apos;re finished
          </Typography>
          <Stack direction="row" sx={{ pt: 2 }}>
            <Stack sx={{ flex: '1 1 auto' }} />
            <Button onClick={handleReset}>Reset</Button>
          </Stack>
        </React.Fragment>
      ) : (
        <React.Fragment>
          <ContactForm step={activeStep} />
          <Stack direction="row" sx={{ pt: 2 }}>
            <Button
              color="inherit"
              disabled={activeStep === 0}
              onClick={handleBack}
              sx={{ mr: 1 }}
            >
              Back
            </Button>
            <Stack sx={{ flex: '1 1 auto' }} />
            <Button onClick={handleNext} sx={{ mr: 1 }}>
              Next
            </Button>
            {activeStep !== steps.length &&
              (completed[activeStep] ? (
                <Typography variant="caption" sx={{ display: 'inline-block' }}>
                  Step {activeStep + 1} already completed
                </Typography>
              ) : (
                <Button onClick={handleComplete}>
                  {completedSteps() === totalSteps() - 1
                    ? 'Finish'
                    : 'Complete Step'}
                </Button>
              ))}
          </Stack>
        </React.Fragment>
      )}
    </Stack >
  );
}
Share this post:

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.