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:

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:

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:

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'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 >
);
}