React Material UI Stepper Form with Formik & Yup
Table of Contents
Overview
This article will break down a step form built with the Material UI and Formik for input validation. The application will be based on an event. The form has four separate forms for user, event with the start & end dates, images, and Terms & Conditions. Screen recording of this application is playing at the bottom of this article.
Download Source from GitHub https://github.com/fullstacksoup
GitHubGetting Started
Create the application if you want to do this from scratch. I recommend cloning the source files in case the library versions have changed.
npx create react-app multi-step-form-demo
Material UI – Version 4
React-Material for the inputs and layout.
Note: Version 5 was released during the development of this article.
For version 4 documentation click here
For the current version documentation click here
All documentation for all versions here
npm install @material-ui/core@4.12.3 @material-ui/icons@4.11.2
npm install @material-ui/lab@4.0.0-alpha.60
npm install @material-ui/pickers@3.3.10
Material Dropzone
material-dropzone is used for uploading with thumbnail previews of multiple files.
npm i material-ui-dropzone
Mask Phone Field
react-text-mask is the library used to create the phone mask. Example: (555) 555-5555

npm i react-text-mask
Formik & Yup
Formik and Yup are used for form validation.
npm i formik
npm i yup
Material Stepper With Formik & Yup
This will be the parent component index.js will handle the multi step form UI logic and field validation for all the forms. The stepper form is take directly from Material UI’s website here if you want more details on stepper forms then go to https://mui.com/components/steppers/#main-content.
Each form will have a Yup validation schema that will be used by the corresponding Formik
Library Imports
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { makeStyles, withStyles } from '@material-ui/core/styles';
import clsx from 'clsx';
import Stepper from '@material-ui/core/Stepper';
import Step from '@material-ui/core/Step';
import StepLabel from '@material-ui/core/StepLabel';
import Check from '@material-ui/icons/Check';
import PlaylistAddCheckIcon from '@material-ui/icons/PlaylistAddCheck';
import PersonIcon from '@material-ui/icons/Person';
import EventIcon from '@material-ui/icons/Event';
import CameraIcon from '@material-ui/icons/Camera';
import StepConnector from '@material-ui/core/StepConnector';
import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import Paper from '@material-ui/core/Paper';
import { useFormik } from 'formik';
import * as yup from 'yup';
import PersonalInfoForm from 'components/stepper-form/PersonalInfoForm';
import EventForm from 'components/stepper-form/EventForm';
import AddImageForm from 'components/stepper-form/AddImageForm';
import TermsConditionsForm from 'components/stepper-form/TermsConditionsForm';
import { format, addDays } from 'date-fns';
Inline Styles
const useStyles = makeStyles((theme) => ({
appBar: {
position: 'relative',
},
layout: {
width: '90vw',
},
paper: {
marginTop: theme.spacing(3),
marginBottom: theme.spacing(3),
padding: theme.spacing(2),
[theme.breakpoints.up(600 + theme.spacing(3) * 2)]: {
marginTop: theme.spacing(6),
marginBottom: theme.spacing(6),
padding: theme.spacing(3),
},
},
stepper: {
padding: theme.spacing(3, 0, 5),
},
buttons: {
display: 'flex',
justifyContent: 'flex-end',
},
button: {
marginTop: theme.spacing(3),
marginLeft: theme.spacing(1),
},
instructions: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));
Material Stepper

The code below is taken directly from Material UI documentation for the stepper form. The functions generate the style, shape, colors based on the status of the step.
function QontoStepIcon(props) {
const classes = useStyles();
const { active, completed } = props;
const [ email, setEmail ] = useState('');
const [ isEmailValid, setIsEmailValid ] = useState(false);
return (
<div
className={clsx(classes.root, {
[classes.active]: active,
})}
>
{completed ? <Check className={classes.completed} /> : <div className={classes.circle} />}
</div>
);
}
QontoStepIcon.propTypes = {
active: PropTypes.bool,
completed: PropTypes.bool,
};
const ColorlibConnector = withStyles({
alternativeLabel: {
top: 22,
},
active: {
'& $line': {
backgroundImage:
'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)',
},
},
completed: {
'& $line': {
backgroundImage:
'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)',
},
},
line: {
height: 3,
border: 0,
backgroundColor: '#eaeaf0',
borderRadius: 1,
},
})(StepConnector);
const useColorlibStepIconStyles = makeStyles({
root: {
backgroundColor: '#ccc',
zIndex: 1,
color: '#fff',
width: 50,
height: 50,
display: 'flex',
borderRadius: '50%',
justifyContent: 'center',
alignItems: 'center',
},
active: {
backgroundImage:
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
boxShadow: '0 4px 10px 0 rgba(0,0,0,.25)',
},
completed: {
backgroundImage:
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
},
});
function ColorlibStepIcon(props) {
const classes = useColorlibStepIconStyles();
const { active, completed } = props;
const icons = {
1: <PersonIcon />,
2: <EventIcon />,
3: <CameraIcon />,
4: <PlaylistAddCheckIcon />
};
return (
<div
className={clsx(classes.root, {
[classes.active]: active,
[classes.completed]: completed,
})}
>
{icons[String(props.icon)]}
</div>
);
}
ColorlibStepIcon.propTypes = {
/**
* Whether this step is active.
*/
active: PropTypes.bool,
/**
* Mark the step as completed. Is passed to child components.
*/
completed: PropTypes.bool,
/**
* The label displayed in the step icon.
*/
icon: PropTypes.node,
};
Formik & Yup
These are in the function for getSteps() and can be modified to add or remove steps along with getStepContent() that is placed inside the main function.
function getSteps() {
return ['Personal Info', 'Additional Info', 'Add Images'];
}
Validation Schemas
Each form has it’s own validation schema for the group of fields that need validation
Personal Information Form
The personal information form will have three required fields (name, email, & phone) and one non-required field (location).
With masked phone number
const phoneRegExp = /^((\+[1-9]{1,4}[ -]?)|(\([0-9]{2,3}\)[ -]?)|([0-9]{2,4})[ -]?)*?[0-9]{3,4}[ -]?[0-9]{3,4}$/;
const personalInfoValidationSchema = yup.object({
name: yup
.string()
.min(2, 'Name should be of minimum 2 characters length')
.required('Name is required'),
email: yup
.string()
.email('Enter a valid email')
.required('Email is required'),
phone: yup
.string()
.matches(phoneRegExp, 'Phone number is not valid')
.required('Phone is required'),
location: yup
.string('Not Required')
});
Event Details Form
The Event form has four fields. The two dates that have to be within 60 days from the current date. The end cannot be a date before the start date. The Event Title and Event Description fields are also required with a minimum and maximum characters rule.
const eventValidationSchema = yup.object({
title: yup
.string()
.min(3, 'Title should be of minimum 3 characters')
.required('Title is required'),
description: yup
.string()
.min(4, 'Description should be of minimum 4 characters')
.required('Description is required'),
startDate: yup
.date()
.min(addDays(new Date(), 1))
.max(addDays(new Date(), 59))
.required('Start Date is required'),
endDate: yup
.date()
.min(
yup.ref('startDate'),
"End date can't be before start date"
)
.max(addDays(new Date(), 60))
.required('End Date is required'),
});
Image Upload Form
This form will have a “Yes” or “No” radio button that is required. The dropzone will be required to have at least one file if the answer is “Yes”. As of the time of this article, I was unable to figure out how to validate the files array. So the dropzone validation is handled with a “useState()” hook for a field labeled “files” and checks the length of the file array to be greater than the initial value of 0. Using Yup, create a field labeled filesCount and set a condition to have it required when the hasImagesToUpload has a value of “Yes”. If the value is “Yes” then use the test() method to check if the files array length is greater than 0.
const imageUploadValidationSchema = yup.object().shape({
hasImagesToUpload: yup
.string()
.required('Do you have any images'),
filesCount: yup
.number()
.when("hasImagesToUpload", {
is: "Yes",
then: yup.number().required('Images are required if you answered Yes above').test(val => val < files.length)
})
});
State Variable
Create a state variable activeStep to keep track of the current step/form.
const [activeStep, setActiveStep] = useState(0);
const steps = getSteps();
Terms & Condition Form
The last form Terms & Conditions will check if the user click the checkbox. If not a Material alert section will show up above the checkbox field.

const termConditionsSchema = yup.object().shape({
isTermChecked: yup
.boolean("Agree to Terms & Conditions")
.required("Please Agree to Terms & Conditions")
.test(val => val !== false)
});
Formik Forms with useFormik Hook
The useFormik() hook validates each form against the Yup validation schemas.
Below are all 4 forms.
const formikPersonalInfo = useFormik({
initialValues: {
name: '',
email: '',
phone: '',
location: '',
},
validationSchema: personalInfoValidationSchema,
onSubmit: values => {
handleNext();
},
});
const formikEvents = useFormik({
initialValues: {
title: '',
description: '',
startDate: format(addDays(new Date(), 1), 'yyyy-MM-dd'),
endDate: format(addDays(new Date(), 31), 'yyyy-MM-dd')
},
validationSchema: eventValidationSchema,
onSubmit: values => {
handleNext();
},
});
const formikImageUpload = useFormik({
initialValues: {
hasImagesToUpload: '',
filesCount: 0
},
validationSchema: imageUploadValidationSchema,
onSubmit: values => {
handleNext();
},
});
const formikTermsConditions = useFormik({
initialValues: {
isTermChecked: false,
},
validationSchema: termConditionsSchema,
onSubmit: values => {
handleFormSubmit();
},
});
Handle Submit
The handleSubmit() function executes the formik validations by the current step.
const handleSubmit = () => {
switch (activeStep) {
case 0: formikPersonalInfo.handleSubmit(); break;
case 1: formikEvents.handleSubmit(); break;
case 2: formikImageUpload.handleSubmit(); break;
case 3: formikTermsConditions.handleSubmit(); break;
}
};
Set and Render Step
Traversing the step counter and the content rendered with handleNext(), handleBack(), and getStepCount(step).
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleReset = () => {
setActiveStep(0);
};
const getStepContent = (step) => {
switch (step) {
case 0:
return (<>
<PersonalInfoForm formik={formikPersonalInfo} />
</>);
case 1:
return (<>
<EventForm formik={formikEvents} />
</>);
case 2:
return (<>
<AddImageForm handleDropzoneChange={handleDropzoneChange}
formik={formikImageUpload}
files={files}
handleDropzoneChange={handleDropzoneChange}
handleIsFileChange={handleIsFileChange}
/>
</>);
case 3:
return (<>
<TermsConditionsForm formik={formikTermsConditions}
/>
</>);
default:
return 'Unknown step';
}
}
Step Titles
These are in the function for getSteps() and can be modified to add or remove steps along with getStepContent() that is placed inside the main function.
function getSteps() {
return ['Personal Info', 'Additional Info', 'Add Images'];
}
Handling Form Submit
handleFormSubmit() is called in the final step and gathers all the inputs to be displayed in JSON data in a JavaScript alert(). Animated Gif below.
const handleFormSubmit = () => {
var data = {
Name: formikPersonalInfo.values.name,
Email: formikPersonalInfo.values.email,
Phone: formikPersonalInfo.values.phone,
Location: formikPersonalInfo.values.location,
EventTitle: formikEvents.values.title,
EventDescription: formikEvents.values.description,
StartDate: formikEvents.values.startDate,
EndDate: formikEvents.values.endDate,
Files: files
}
console.log('handleFormSubmit ', data);
alert(JSON.stringify(data, null, 2));
}

The Rest of the Parent Form
The rest of the parent form has functions to handle the files array, has files for validation, and Terms & Agreements checkbox.
const handleDropzoneChange = (files) => {
setFiles(files);
}
const handleIsFileChange = (val) => {
setHasFiles(val);
}
const handleIsTermChecked = (val) => {
setIsTermChecked(val);
}
return (
<React.Fragment>
<Grid container spacing={3}>
<Grid item xs={12} lg={3} xl={3}></Grid>
<Grid item xs={12} lg={6} xl={6}>
<Paper className={classes.paper}>
<form noValidate autoComplete="off">
<Stepper alternativeLabel activeStep={activeStep} connector={<ColorlibConnector />}>
{steps.map((label) => (
<Step key={label}>
<StepLabel StepIconComponent={ColorlibStepIcon}>{label}</StepLabel>
</Step>
))}
</Stepper>
<div>
{activeStep === steps.length ? (
<div>
<Typography className={classes.instructions}>
All steps completed - you're finished
</Typography>
<Button onClick={handleReset} className={classes.button}>
Reset
</Button>
</div>
) : (
<div>
<Typography className={classes.instructions}>{getStepContent(activeStep)}</Typography>
<React.Fragment>
<div className={classes.buttons}>
<Button disabled={activeStep === 0} onClick={handleBack} className={classes.button}>
Back
</Button>
{activeStep === steps.length - 1 ?
<Button
variant="contained"
// disabled={!isStepValidated(activeStep) }
color="primary"
onClick={handleSubmit}
className={classes.button}
>
Finish
</Button>
:
<Button
variant="contained"
color="primary"
onClick={handleSubmit}
className={classes.button}
>
Next
</Button>
}
</div>
</React.Fragment>
</div>
)}
</div>
</form>
</Paper>
</Grid>
<Grid item xs={12} lg={3} xl={3}></Grid>
</Grid>
</React.Fragment>
);
}
The Step Forms
There are four individual forms in this example.
- PersonalInfoForm.js
- EventForm.js
- AddImagesForm.js
- TermsConditionsForm.js

Personal Information Form
This form displays the 4 fields for name, email, phone, and location. All of the fields except for location will be validated before a user can move on to the next form.
Each input uses formik to handle onChange(), onBlur() to check if if the input is pristine, error to highlight the input in red, helperText for the error message as shown below.
onChange={props.formik.handleChange}
onBlur={props.formik.handleBlur}
value={props.formik.values.name}
error={props.formik.touched.name && Boolean(props.formik.errors.name)}
helperText={props.formik.touched.name && props.formik.errors.name}
Code for the entire Personal Information Form
import React from 'react'
import PropTypes from 'prop-types';
import MaskedInput from 'react-text-mask';
import Grid from '@material-ui/core/Grid';
import Container from '@material-ui/core/Container';
import TextField from '@material-ui/core/TextField';
import InputLabel from '@material-ui/core/InputLabel';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import OutlinedInput from '@material-ui/core/OutlinedInput';
function TextMaskCustom(props) {
const { inputRef, ...other } = props;
return (
<MaskedInput
{...other}
ref={(ref) => {
inputRef(ref ? ref.inputElement : null);
}}
mask={['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/]}
placeholderChar={'\u2000'}
showMask
/>
);
}
TextMaskCustom.propTypes = {
inputRef: PropTypes.func.isRequired,
};
export default function PersonalInfoForm(props) {
return (
<>
<Container maxWidth="sm">
<Grid container spacing={3}>
<Grid item xs={6} >
<TextField
fullWidth
variant="outlined"
size="small"
label="Name"
id="name"
name="name"
type="text"
onChange={props.formik.handleChange}
onBlur={props.formik.handleBlur}
value={props.formik.values.name}
error={props.formik.touched.name && Boolean(props.formik.errors.name)}
helperText={props.formik.touched.name && props.formik.errors.name}
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Grid item xs={1} ></Grid>
<Grid item xs={5} >
<TextField
fullWidth
variant="outlined"
size="small"
label="Location"
id="location"
name="location"
type="text"
onChange={props.formik.handleChange}
onBlur={props.formik.handleBlur}
value={props.formik.values.location}
helperText="(Optional)"
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Grid item xs={6} >
<TextField
fullWidth
size="small"
variant="outlined"
label="Email"
id="email"
name="email"
type="email"
onChange={props.formik.handleChange}
onBlur={props.formik.handleBlur}
value={props.formik.values.email}
error={props.formik.touched.email && Boolean(props.formik.errors.email)}
helperText={props.formik.touched.email && props.formik.errors.email}
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Grid item xs={1} ></Grid>
<Grid item xs={5} >
<FormControl variant="outlined" size={'small'} fullWidth>
<InputLabel htmlFor="phone">Phone</InputLabel>
<OutlinedInput
value={props.formik.values.phone}
error={props.formik.touched.phone && Boolean(props.formik.errors.phone)}
helperText={props.formik.touched.phone && props.formik.errors.phone}
onChange={props.formik.handleChange}
name="phone"
id="phone"
label="Phone"
size={'small'}
style={{marginTop: -1}}
InputLabelProps={{
shrink: true,
}}
inputComponent={TextMaskCustom}
/>
<FormHelperText id="my-helper-text" error={props.formik.touched.phone && Boolean(props.formik.errors.phone)}>
{props.formik.errors.phone}
</FormHelperText>
</FormControl>
</Grid>
</Grid>
</Container>
</>
)
}
Event Form
The Event Form has a title, description, and two dates fields that are required.
import React from 'react';
import Grid from '@material-ui/core/Grid';
import Container from '@material-ui/core/Container';
import TextField from '@material-ui/core/TextField';
import styles from 'styles/EventForm.module.css';
export default function EventForm(props) {
return (
< >
<Container maxWidth="sm">
<Grid container spacing={3}>
<Grid item xs={12} >
<TextField placeholder=""
label="Event Title"
fullWidth={true}
variant="outlined"
size="small"
type="text"
name="title"
className={styles.textField}
onChange={props.formik.handleChange}
onBlur={props.formik.handleBlur}
value={props.formik.values.title}
error={props.formik.touched.title && Boolean(props.formik.errors.title)}
helperText={props.formik.touched.title && props.formik.errors.title}
/>
</Grid>
<Grid item xs={12} >
<TextField label="Event Details"
variant="outlined"
fullWidth={true}
size="small"
type="text"
name="description"
multiline
minRows={4}
className={styles.textField}
onChange={props.formik.handleChange}
onBlur={props.formik.handleBlur}
value={props.formik.values.description}
error={props.formik.touched.description && Boolean(props.formik.errors.description)}
helperText={props.formik.touched.description && props.formik.errors.description}
/>
</Grid>
<Grid sm={5}>
<TextField label="Start Date"
variant="outlined"
size="small"
name="startDate"
id="startDate"
type="date"
style={{marginTop: '17px', marginLeft: '12px'}}
defaultValue={props.formik.values.startDate}
onChange={props.formik.handleChange}
onBlur={props.formik.handleBlur}
value={props.formik.values.startDate}
error={props.formik.touched.startDate && Boolean(props.formik.errors.startDate)}
helperText={props.formik.touched.startDate && props.formik.errors.startDate}
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Grid sm={2}></Grid>
<Grid sm={5} align="right">
<TextField label="End Date"
variant="outlined"
size="small"
name="endDate"
id="endDate"
type="date"
style={{marginTop: '17px', marginRight: '12px'}}
defaultValue={props.formik.values.endDate}
onChange={props.formik.handleChange}
onBlur={props.formik.handleBlur}
value={props.formik.values.endDate}
error={props.formik.touched.endDate && Boolean(props.formik.errors.endDate)}
helperText={props.formik.touched.endDate && props.formik.errors.endDate}
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
</Grid>
</Container>
</>
)
}
Add Images Form
import React from 'react';
import { DropzoneArea } from 'material-ui-dropzone';
import FormControl from '@material-ui/core/FormControl';
import Radio from '@material-ui/core/Radio';
import RadioGroup from '@material-ui/core/RadioGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormLabel from '@material-ui/core/FormLabel';
import AttachFile from '@material-ui/icons/PhotoCamera';
import styles from 'styles/UploadFileForm.module.css';
import Container from '@material-ui/core/Container';
import Alert from '@material-ui/lab/Alert';
export default function AddImageForm(props) {
return (
<React.Fragment>
<Container maxWidth="sm">
<FormControl component="fieldset" className={styles.formControl}>
{(props.formik.touched.hasImagesToUpload && Boolean(props.formik.errors.hasImagesToUpload)) ?
<>
<FormLabel component="legend" style={{color: 'red'}}>Do you have any images? (Required)</FormLabel>
</>
:
<>
<FormLabel component="legend" color="error">Do you have any images?</FormLabel>
</>
}
<RadioGroup row aria-label="position"
name="hasImagesToUpload"
onChange={props.formik.handleChange}
onBlur={props.formik.handleBlur}
value={props.formik.values.hasImagesToUpload}
error={props.formik.touched.hasImagesToUpload && Boolean(props.formik.errors.hasImagesToUpload)}
helperText={props.formik.touched.hasImagesToUpload && props.formik.errors.hasImagesToUpload}
>
<FormControlLabel
value="Yes"
control={<Radio color="primary" />}
label="Yes"
labelPlacement="start"
/>
<FormControlLabel
value="No"
control={<Radio color="primary" />}
label="No"
labelPlacement="start"
/>
</RadioGroup>
</FormControl>
{(props.formik.touched.filesCount && Boolean(props.formik.errors.filesCount) && props.files.length === 0)?
<Alert severity="error">Please upload 1 to 3 images</Alert>
:
''
}
{props.formik.values.hasImagesToUpload == "Yes"?
<DropzoneArea filesLimit={3}
//onChange={props.formik.handleChange}
previewText="Selected files"
// useChipsForPreview
onChange={props.handleDropzoneChange }
// values={props.files}
initialFiles={props.files}
// initialFiles={props.formik.values.files}
Icon={AttachFile}
acceptedFiles={['image/*']}
showAlerts={['error']}
dropzoneText="Click here to upload photo"
dropzoneClass={styles.dropZoneCls} />
: ''}
</Container>
</React.Fragment>
)
}
Terms & Conditions – Last Form
Finally, the last form which requires a checkbox to be clicked before a submit. The submit will show the input as JSON data in an alert() popup.
import React from 'react';
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';
import Checkbox from '@material-ui/core/Checkbox';
import Alert from '@material-ui/lab/Alert';
export default function TermsConditionsForm(props) {
return (
<div >
<Box component="div" m={8} align="left">
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
when an unknown printer took a galley of type and scrambled it to make a type specimen book.
It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,
and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsu
</Box>
{(props.formik.touched.isTermChecked && Boolean(props.formik.errors.isTermChecked))?
<Alert severity="error">Please agree to the Terms & Conditions </Alert>
:
''
}
<Typography variant="h6" align="center">
Terms & Conditions
<Checkbox
name="isTermChecked"
checked={props.formik.values.isTermChecked}
onChange={props.formik.handleChange}
onBlur={props.formik.handleBlur}
inputProps={{ 'aria-label': 'primary checkbox' }} />
</Typography>
</div>
)
}
That’s it.
Please get the source files from
Quick overview of the app.

You must be logged in to post a comment.