Building Vertical Form Layouts with MUI Stack: A Complete Guide
Forms are the bread and butter of interactive web applications, but creating well-structured, responsive form layouts can be challenging. Material UI's Stack component offers an elegant solution for building vertical form layouts with precise spacing control. In this article, I'll walk you through how to leverage the Stack component to create clean, maintainable form layouts that look professional and adapt seamlessly across devices.
What You'll Learn
By the end of this guide, you'll know how to:
- Implement vertical form layouts using MUI's Stack component
- Control spacing between form elements consistently
- Create responsive form layouts that work across devices
- Customize Stack to match your design requirements
- Combine Stack with other MUI components for complex form patterns
- Avoid common pitfalls when building form layouts
Understanding MUI Stack Component
The Stack component is one of Material UI's most useful layout primitives. It's essentially a wrapper component that manages the spacing and alignment between its children along a single direction - either vertical or horizontal. For form layouts, the vertical orientation is particularly useful.
Core Concepts of Stack
Stack is fundamentally a flexbox container with added convenience features. It abstracts away many of the complexities of CSS flexbox, letting you focus on your form's structure rather than wrestling with CSS. The component was introduced to solve the common problem of maintaining consistent spacing between elements.
When building forms, Stack helps maintain visual rhythm through consistent spacing, making your forms more readable and professional. It also simplifies the creation of responsive layouts by handling alignment and spacing adaptively.
Key Props and Configuration Options
Stack offers several important props that control its behavior. Here's a comprehensive breakdown of the essential props you'll use when building form layouts:
Prop | Type | Default | Description |
---|---|---|---|
direction | 'column' | 'column-reverse' | 'row' | 'row-reverse' | object | 'column' | Defines the flex direction. For vertical forms, use 'column'. |
spacing | number | string | object | 0 | Spacing between items (in px, or theme spacing units) |
divider | node | undefined | Optional element to insert between items |
children | node | - | The content of the stack - your form elements |
alignItems | 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline' | 'stretch' | Aligns items along the cross axis |
justifyContent | 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly' | 'flex-start' | Aligns items along the main axis |
useFlexGap | boolean | false | If true, uses CSS flex gap for spacing instead of margin |
Understanding these props is crucial for building effective form layouts. The direction
prop determines the stacking direction, while spacing
controls the gap between form elements. For vertical forms, we typically use direction="column"
and adjust the spacing based on design requirements.
Stack vs. Other Layout Options
Before diving into implementation, it's worth understanding why Stack is often the best choice for form layouts compared to alternatives:
-
Stack vs. Grid: While Grid is powerful for complex two-dimensional layouts, it's often overkill for simple vertical forms. Stack provides a more straightforward API for one-dimensional layouts.
-
Stack vs. Box with margin: Using Box components with margins works, but Stack centralizes spacing logic and makes it easier to maintain consistent spacing throughout the form.
-
Stack vs. custom CSS: Stack eliminates the need for custom CSS classes or inline styles for layout, making your code cleaner and more maintainable.
For vertical form layouts specifically, Stack strikes the perfect balance between simplicity and flexibility.
Setting Up Your Form Structure with Stack
Let's start by creating a basic vertical form structure using Stack. The first step is to install the required dependencies.
Installation and Setup
If you haven't already installed Material UI in your React project, you'll need to do that first:
npm install @mui/material @emotion/react @emotion/styled
For a complete form, you'll also want to add form control components:
npm install @mui/icons-material
Now, let's create a basic form structure with Stack:
import React from 'react';
import {
Stack,
TextField,
Button,
Typography,
FormControlLabel,
Checkbox
} from '@mui/material';
function VerticalForm() {
return (
<Stack
component="form"
spacing={2}
sx={{ width: '100%', maxWidth: 500 }}
noValidate
autoComplete="off"
>
<Typography variant="h5" component="h2">
Contact Information
</Typography>
<TextField
label="Full Name"
variant="outlined"
fullWidth
/>
<TextField
label="Email"
variant="outlined"
type="email"
fullWidth
/>
<TextField
label="Message"
variant="outlined"
multiline
rows={4}
fullWidth
/>
<FormControlLabel
control={<Checkbox />}
label="Subscribe to newsletter"
/>
<Button
variant="contained"
color="primary"
type="submit"
sx={{ mt: 2 }}
>
Submit
</Button>
</Stack>
);
}
export default VerticalForm;
This code creates a simple contact form with a title, three text fields, a checkbox, and a submit button. The spacing={2}
prop adds uniform spacing between all elements, creating a clean vertical layout.
Understanding the Stack Structure
Let's analyze what's happening in this form:
- The
component="form"
prop makes the Stack render as a<form>
element instead of a<div>
. - The
spacing={2}
prop adds 16px of space between each child (in the default theme, 1 spacing unit = 8px). - The
sx
prop applies custom styling, setting a maximum width for the form. - Each form control is a direct child of Stack, so they're arranged vertically with equal spacing.
This approach creates a clean, consistent layout with minimal code. The Stack component handles all the spacing automatically, so you don't need to add margin properties to individual components.
Controlling Spacing in Vertical Forms
One of Stack's most powerful features is its flexible spacing system. Let's explore how to use it effectively in form layouts.
Understanding the Spacing Prop
The spacing
prop in Stack is incredibly versatile. It accepts:
- Numbers: Interpreted as multipliers of the theme spacing unit (typically 8px)
- Strings: CSS values like '10px' or '1rem'
- Objects: For responsive spacing that changes with breakpoints
Let's see examples of each:
// Using a number (theme spacing)
<Stack spacing={2}>
{/* 16px spacing (2 × 8px) */}
</Stack>
// Using a string (direct CSS value)
<Stack spacing="24px">
{/* Exactly 24px spacing */}
</Stack>
// Using an object for responsive spacing
<Stack spacing={{ xs: 1, sm: 2, md: 3 }}>
{/*
8px spacing on extra-small screens
16px spacing on small screens
24px spacing on medium and larger screens
*/}
</Stack>
For most form layouts, I recommend using the numeric values that align with your theme spacing. This ensures consistency across your application.
Creating Visual Hierarchy with Varied Spacing
Not all elements in a form should have the same spacing. You can create visual hierarchy by grouping related fields and using different spacing values:
import React from 'react';
import {
Stack,
TextField,
Button,
Typography,
Divider
} from '@mui/material';
function AddressForm() {
return (
<Stack spacing={3} sx={{ width: '100%', maxWidth: 500 }}>
<Typography variant="h5">Shipping Information</Typography>
{/* Name fields with closer spacing */}
<Stack direction="row" spacing={2}>
<TextField label="First Name" fullWidth />
<TextField label="Last Name" fullWidth />
</Stack>
<TextField label="Email Address" fullWidth />
<TextField label="Phone Number" fullWidth />
<Divider />
<Typography variant="h6">Address</Typography>
<TextField label="Street Address" fullWidth />
{/* City, state, zip with closer spacing */}
<Stack direction="row" spacing={2}>
<TextField label="City" fullWidth />
<TextField label="State" fullWidth />
<TextField label="ZIP Code" fullWidth />
</Stack>
<Button variant="contained" color="primary">
Continue to Payment
</Button>
</Stack>
);
}
export default AddressForm;
In this example, we use nested Stack components to create a more complex layout:
- The outer Stack has
spacing={3}
for major sections - We group first name and last name in a horizontal Stack with
spacing={2}
- Similarly, we group city, state, and ZIP in another row
- We use a Divider to visually separate sections
This creates a form with clear visual hierarchy, making it easier for users to understand the structure.
Responsive Spacing Strategies
Forms often need to adapt to different screen sizes. Stack makes this easy with responsive spacing:
import React from 'react';
import {
Stack,
TextField,
Button,
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
function ResponsiveForm() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
return (
<Stack
spacing={{ xs: 1.5, sm: 2, md: 3 }}
sx={{ width: '100%', maxWidth: 600 }}
>
<Typography variant="h5">Profile Information</Typography>
{/* Responsive direction based on screen size */}
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={{ xs: 1.5, sm: 2 }}
>
<TextField label="First Name" fullWidth />
<TextField label="Last Name" fullWidth />
</Stack>
<TextField label="Email" fullWidth />
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={{ xs: 1.5, sm: 2 }}
>
<TextField label="Password" type="password" fullWidth />
<TextField label="Confirm Password" type="password" fullWidth />
</Stack>
<Button
variant="contained"
fullWidth={isMobile}
sx={{ alignSelf: isMobile ? 'stretch' : 'flex-start' }}
>
Create Account
</Button>
</Stack>
);
}
export default ResponsiveForm;
This form adapts to different screen sizes in several ways:
- The main Stack uses responsive spacing that increases on larger screens
- The name fields and password fields switch between row and column layout based on screen size
- The button width and alignment change on mobile devices
This approach ensures the form remains usable and visually balanced across all devices.
Building a Complete Vertical Form with Stack
Now that we understand the basics, let's build a more complete form example that showcases Stack's capabilities for vertical layouts.
Step 1: Create the Form Structure
First, let's create the overall structure of our form:
import React, { useState } from 'react';
import {
Stack,
Typography,
TextField,
FormControl,
FormLabel,
RadioGroup,
Radio,
FormControlLabel,
Checkbox,
MenuItem,
Button,
Divider,
Box,
Paper
} from '@mui/material';
function JobApplicationForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
phone: '',
position: '',
experience: '',
employmentType: 'full-time',
skills: [],
portfolio: '',
coverLetter: '',
agreeToTerms: false
});
const handleChange = (event) => {
const { name, value, checked, type } = event.target;
setFormData(prevData => ({
...prevData,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleSubmit = (event) => {
event.preventDefault();
console.log('Form submitted:', formData);
// In a real app, you would send this data to your server
};
return (
<Paper elevation={3} sx={{ p: 4, maxWidth: 800, mx: 'auto' }}>
<Stack component="form" spacing={4} onSubmit={handleSubmit}>
<Typography variant="h4" component="h1" gutterBottom>
Job Application Form
</Typography>
{/* Form content will go here */}
<Button
type="submit"
variant="contained"
size="large"
sx={{ mt: 2 }}
>
Submit Application
</Button>
</Stack>
</Paper>
);
}
export default JobApplicationForm;
This code sets up the basic structure with state management for our form. Now, let's add the different sections of the form.
Step 2: Add Personal Information Section
Let's add the first section for personal details:
// Inside the main Stack, after the Typography heading
<Stack spacing={3}>
<Typography variant="h5" component="h2">
Personal Information
</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="First Name"
name="firstName"
value={formData.firstName}
onChange={handleChange}
fullWidth
required
/>
<TextField
label="Last Name"
name="lastName"
value={formData.lastName}
onChange={handleChange}
fullWidth
required
/>
</Stack>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
fullWidth
required
/>
<TextField
label="Phone"
name="phone"
type="tel"
value={formData.phone}
onChange={handleChange}
fullWidth
/>
</Stack>
</Stack>
<Divider />
Here, we've created a section with:
- A heading for the section
- Two rows of fields, each containing two inputs
- Responsive layout that stacks vertically on small screens
- A divider to separate this section from the next
Step 3: Add Job Details Section
Now let's add a section for job-related information:
// After the divider
<Stack spacing={3}>
<Typography variant="h5" component="h2">
Job Details
</Typography>
<TextField
select
label="Position Applying For"
name="position"
value={formData.position}
onChange={handleChange}
fullWidth
required
>
<MenuItem value="developer">Software Developer</MenuItem>
<MenuItem value="designer">UI/UX Designer</MenuItem>
<MenuItem value="manager">Project Manager</MenuItem>
<MenuItem value="analyst">Business Analyst</MenuItem>
</TextField>
<TextField
select
label="Years of Experience"
name="experience"
value={formData.experience}
onChange={handleChange}
fullWidth
required
>
<MenuItem value="0-1">0-1 years</MenuItem>
<MenuItem value="1-3">1-3 years</MenuItem>
<MenuItem value="3-5">3-5 years</MenuItem>
<MenuItem value="5+">5+ years</MenuItem>
</TextField>
<FormControl component="fieldset">
<FormLabel component="legend">Employment Type</FormLabel>
<RadioGroup
name="employmentType"
value={formData.employmentType}
onChange={handleChange}
row
>
<FormControlLabel
value="full-time"
control={<Radio />}
label="Full-time"
/>
<FormControlLabel
value="part-time"
control={<Radio />}
label="Part-time"
/>
<FormControlLabel
value="contract"
control={<Radio />}
label="Contract"
/>
</RadioGroup>
</FormControl>
</Stack>
<Divider />
This section demonstrates how to integrate different form controls within the Stack:
- Select fields for position and experience
- A radio button group for employment type
- Consistent spacing between all elements
Step 4: Add Additional Information Section
Finally, let's add a section for additional information:
// After the second divider
<Stack spacing={3}>
<Typography variant="h5" component="h2">
Additional Information
</Typography>
<TextField
label="Portfolio URL"
name="portfolio"
value={formData.portfolio}
onChange={handleChange}
placeholder="https://yourportfolio.com"
fullWidth
/>
<TextField
label="Cover Letter"
name="coverLetter"
value={formData.coverLetter}
onChange={handleChange}
multiline
rows={4}
fullWidth
placeholder="Tell us why you're interested in this position..."
/>
<FormControlLabel
control={
<Checkbox
checked={formData.agreeToTerms}
onChange={handleChange}
name="agreeToTerms"
required
/>
}
label="I agree to the terms and conditions"
/>
</Stack>
This section includes:
- A text field for a portfolio URL
- A multiline text field for a cover letter
- A checkbox for terms and conditions agreement
Step 5: Put It All Together
Now let's combine all sections into our complete form:
import React, { useState } from 'react';
import {
Stack,
Typography,
TextField,
FormControl,
FormLabel,
RadioGroup,
Radio,
FormControlLabel,
Checkbox,
MenuItem,
Button,
Divider,
Paper
} from '@mui/material';
function JobApplicationForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
phone: '',
position: '',
experience: '',
employmentType: 'full-time',
portfolio: '',
coverLetter: '',
agreeToTerms: false
});
const handleChange = (event) => {
const { name, value, checked, type } = event.target;
setFormData(prevData => ({
...prevData,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleSubmit = (event) => {
event.preventDefault();
console.log('Form submitted:', formData);
// In a real app, you would send this data to your server
};
return (
<Paper elevation={3} sx={{ p: 4, maxWidth: 800, mx: 'auto' }}>
<Stack component="form" spacing={4} onSubmit={handleSubmit}>
<Typography variant="h4" component="h1" gutterBottom>
Job Application Form
</Typography>
{/* Personal Information Section */}
<Stack spacing={3}>
<Typography variant="h5" component="h2">
Personal Information
</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="First Name"
name="firstName"
value={formData.firstName}
onChange={handleChange}
fullWidth
required
/>
<TextField
label="Last Name"
name="lastName"
value={formData.lastName}
onChange={handleChange}
fullWidth
required
/>
</Stack>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
fullWidth
required
/>
<TextField
label="Phone"
name="phone"
type="tel"
value={formData.phone}
onChange={handleChange}
fullWidth
/>
</Stack>
</Stack>
<Divider />
{/* Job Details Section */}
<Stack spacing={3}>
<Typography variant="h5" component="h2">
Job Details
</Typography>
<TextField
select
label="Position Applying For"
name="position"
value={formData.position}
onChange={handleChange}
fullWidth
required
>
<MenuItem value="developer">Software Developer</MenuItem>
<MenuItem value="designer">UI/UX Designer</MenuItem>
<MenuItem value="manager">Project Manager</MenuItem>
<MenuItem value="analyst">Business Analyst</MenuItem>
</TextField>
<TextField
select
label="Years of Experience"
name="experience"
value={formData.experience}
onChange={handleChange}
fullWidth
required
>
<MenuItem value="0-1">0-1 years</MenuItem>
<MenuItem value="1-3">1-3 years</MenuItem>
<MenuItem value="3-5">3-5 years</MenuItem>
<MenuItem value="5+">5+ years</MenuItem>
</TextField>
<FormControl component="fieldset">
<FormLabel component="legend">Employment Type</FormLabel>
<RadioGroup
name="employmentType"
value={formData.employmentType}
onChange={handleChange}
row
>
<FormControlLabel
value="full-time"
control={<Radio />}
label="Full-time"
/>
<FormControlLabel
value="part-time"
control={<Radio />}
label="Part-time"
/>
<FormControlLabel
value="contract"
control={<Radio />}
label="Contract"
/>
</RadioGroup>
</FormControl>
</Stack>
<Divider />
{/* Additional Information Section */}
<Stack spacing={3}>
<Typography variant="h5" component="h2">
Additional Information
</Typography>
<TextField
label="Portfolio URL"
name="portfolio"
value={formData.portfolio}
onChange={handleChange}
placeholder="https://yourportfolio.com"
fullWidth
/>
<TextField
label="Cover Letter"
name="coverLetter"
value={formData.coverLetter}
onChange={handleChange}
multiline
rows={4}
fullWidth
placeholder="Tell us why you're interested in this position..."
/>
<FormControlLabel
control={
<Checkbox
checked={formData.agreeToTerms}
onChange={handleChange}
name="agreeToTerms"
required
/>
}
label="I agree to the terms and conditions"
/>
</Stack>
<Button
type="submit"
variant="contained"
size="large"
sx={{ mt: 2 }}
>
Submit Application
</Button>
</Stack>
</Paper>
);
}
export default JobApplicationForm;
This complete form demonstrates several important patterns:
- Nested Stack components: We use a main Stack for the overall form, and nested Stacks for each section and row of inputs.
- Varied spacing: The main sections have more spacing (
spacing={4}
), while elements within sections have less (spacing={3}
). - Responsive layouts: Name and contact fields switch between row and column layout based on screen size.
- Visual separation: We use Dividers to clearly separate the form sections.
Advanced Stack Customization for Forms
Now that we've built a basic form, let's explore some advanced customization techniques for Stack-based forms.
Custom Styling with the sx Prop
The sx
prop is a powerful way to customize Stack's appearance:
<Stack
spacing={2}
sx={{
p: 3,
bgcolor: 'background.paper',
borderRadius: 2,
boxShadow: '0 1px 3px rgba(0,0,0,0.12)',
'& .MuiTextField-root': {
bgcolor: 'background.default'
}
}}
>
<TextField label="Username" />
<TextField label="Password" type="password" />
</Stack>
This example:
- Adds padding around the Stack
- Sets a background color
- Adds rounded corners and a subtle shadow
- Styles all TextField components inside the Stack
The sx
prop accepts any CSS properties and has access to your theme values, making it extremely flexible.
Creating Dynamic Spacing with Dividers
Stack's divider
prop lets you insert elements between each child:
import React from 'react';
import {
Stack,
TextField,
Divider,
Typography
} from '@mui/material';
function FormWithDividers() {
return (
<Stack
spacing={3}
divider={<Divider flexItem />}
sx={{ width: '100%', maxWidth: 500 }}
>
<Typography variant="h6">Personal Details</Typography>
<TextField label="Full Name" fullWidth />
<TextField label="Email" type="email" fullWidth />
<Typography variant="h6">Address Information</Typography>
<TextField label="Street Address" fullWidth />
<TextField label="City" fullWidth />
<Typography variant="h6">Additional Information</Typography>
<TextField label="Comments" multiline rows={3} fullWidth />
</Stack>
);
}
export default FormWithDividers;
This creates a form with dividers automatically inserted between each element, creating clear visual separation. The flexItem
prop ensures the dividers stretch to full width.
Creating Conditional Layouts
Sometimes you need different layouts based on form state. Stack makes this easy:
import React, { useState } from 'react';
import {
Stack,
TextField,
FormControlLabel,
Switch,
Typography
} from '@mui/material';
function ConditionalForm() {
const [hasShippingAddress, setHasShippingAddress] = useState(false);
return (
<Stack spacing={3} sx={{ width: '100%', maxWidth: 600 }}>
<Typography variant="h6">Billing Address</Typography>
<TextField label="Street Address" fullWidth />
<TextField label="City" fullWidth />
<FormControlLabel
control={
<Switch
checked={hasShippingAddress}
onChange={(e) => setHasShippingAddress(e.target.checked)}
/>
}
label="Ship to a different address"
/>
{hasShippingAddress && (
<Stack spacing={3} sx={{ mt: 2 }}>
<Typography variant="h6">Shipping Address</Typography>
<TextField label="Street Address" fullWidth />
<TextField label="City" fullWidth />
</Stack>
)}
</Stack>
);
}
export default ConditionalForm;
This form shows a shipping address section only when the user toggles the switch. The nested Stack maintains proper spacing in both states.
Integrating Stack with Form Libraries
For complex forms, you might want to use a form library like Formik or React Hook Form. Let's see how to integrate Stack with these libraries.
Using Stack with Formik
Formik is a popular form library that handles form state, validation, and submission. Here's how to use it with Stack:
import React from 'react';
import {
Stack,
TextField,
Button,
Typography
} from '@mui/material';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
// Validation schema
const SignupSchema = Yup.object().shape({
firstName: Yup.string()
.min(2, 'Too Short!')
.max(50, 'Too Long!')
.required('Required'),
lastName: Yup.string()
.min(2, 'Too Short!')
.max(50, 'Too Long!')
.required('Required'),
email: Yup.string()
.email('Invalid email')
.required('Required'),
password: Yup.string()
.min(8, 'Password must be at least 8 characters')
.required('Required'),
});
function FormikStackForm() {
return (
<Formik
initialValues={{
firstName: '',
lastName: '',
email: '',
password: '',
}}
validationSchema={SignupSchema}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 400);
}}
>
{({ errors, touched, isSubmitting, getFieldProps }) => (
<Form>
<Stack spacing={3} sx={{ width: '100%', maxWidth: 500 }}>
<Typography variant="h5">Sign Up</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
fullWidth
label="First Name"
{...getFieldProps('firstName')}
error={touched.firstName && Boolean(errors.firstName)}
helperText={touched.firstName && errors.firstName}
/>
<TextField
fullWidth
label="Last Name"
{...getFieldProps('lastName')}
error={touched.lastName && Boolean(errors.lastName)}
helperText={touched.lastName && errors.lastName}
/>
</Stack>
<TextField
fullWidth
label="Email"
type="email"
{...getFieldProps('email')}
error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email}
/>
<TextField
fullWidth
label="Password"
type="password"
{...getFieldProps('password')}
error={touched.password && Boolean(errors.password)}
helperText={touched.password && errors.password}
/>
<Button
type="submit"
variant="contained"
disabled={isSubmitting}
>
Submit
</Button>
</Stack>
</Form>
)}
</Formik>
);
}
export default FormikStackForm;
This example shows how Stack provides the layout structure while Formik handles form state and validation. The key points:
- We use Formik's
Form
component as the form element - Stack handles the spacing and layout
- TextField components receive props from Formik via
getFieldProps
- Error states and messages are handled by MUI's built-in error props
Using Stack with React Hook Form
React Hook Form is another popular library that focuses on performance. Here's how to use it with Stack:
import React from 'react';
import {
Stack,
TextField,
Button,
Typography
} from '@mui/material';
import { useForm, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
// Validation schema
const schema = yup.object({
firstName: yup.string().required('First name is required'),
lastName: yup.string().required('Last name is required'),
email: yup.string().email('Enter a valid email').required('Email is required'),
password: yup.string()
.min(8, 'Password must be at least 8 characters')
.required('Password is required'),
}).required();
function ReactHookFormStack() {
const { control, handleSubmit, formState: { errors } } = useForm({
resolver: yupResolver(schema),
defaultValues: {
firstName: '',
lastName: '',
email: '',
password: '',
}
});
const onSubmit = data => console.log(data);
return (
<Stack
component="form"
onSubmit={handleSubmit(onSubmit)}
spacing={3}
sx={{ width: '100%', maxWidth: 500 }}
>
<Typography variant="h5">Sign Up</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<Controller
name="firstName"
control={control}
render={({ field }) => (
<TextField
{...field}
label="First Name"
fullWidth
error={!!errors.firstName}
helperText={errors.firstName?.message}
/>
)}
/>
<Controller
name="lastName"
control={control}
render={({ field }) => (
<TextField
{...field}
label="Last Name"
fullWidth
error={!!errors.lastName}
helperText={errors.lastName?.message}
/>
)}
/>
</Stack>
<Controller
name="email"
control={control}
render={({ field }) => (
<TextField
{...field}
label="Email"
type="email"
fullWidth
error={!!errors.email}
helperText={errors.email?.message}
/>
)}
/>
<Controller
name="password"
control={control}
render={({ field }) => (
<TextField
{...field}
label="Password"
type="password"
fullWidth
error={!!errors.password}
helperText={errors.password?.message}
/>
)}
/>
<Button type="submit" variant="contained">
Submit
</Button>
</Stack>
);
}
export default ReactHookFormStack;
With React Hook Form, we use the Controller
component to connect form fields to the form state. The Stack component again provides the layout structure.
Advanced Form Patterns with Stack
Let's explore some advanced form patterns you can implement with Stack.
Multi-step Forms
Multi-step forms break complex forms into manageable sections. Stack helps maintain consistent layouts across steps:
import React, { useState } from 'react';
import {
Stack,
TextField,
Button,
Typography,
Stepper,
Step,
StepLabel,
Paper
} from '@mui/material';
function MultiStepForm() {
const [activeStep, setActiveStep] = useState(0);
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
address: '',
city: '',
zipCode: '',
cardName: '',
cardNumber: '',
expDate: '',
cvv: ''
});
const steps = ['Personal Information', 'Address', 'Payment Details'];
const handleNext = () => {
setActiveStep(prevStep => prevStep + 1);
};
const handleBack = () => {
setActiveStep(prevStep => prevStep - 1);
};
const handleChange = (event) => {
const { name, value } = event.target;
setFormData(prevData => ({
...prevData,
[name]: value
}));
};
const handleSubmit = (event) => {
event.preventDefault();
if (activeStep < steps.length - 1) {
handleNext();
} else {
console.log('Form submitted:', formData);
// Submit the form data
}
};
// Content for each step
const getStepContent = (step) => {
switch (step) {
case 0:
return (
<Stack spacing={3}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="First Name"
name="firstName"
value={formData.firstName}
onChange={handleChange}
fullWidth
required
/>
<TextField
label="Last Name"
name="lastName"
value={formData.lastName}
onChange={handleChange}
fullWidth
required
/>
</Stack>
<TextField
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
fullWidth
required
/>
</Stack>
);
case 1:
return (
<Stack spacing={3}>
<TextField
label="Address"
name="address"
value={formData.address}
onChange={handleChange}
fullWidth
required
/>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="City"
name="city"
value={formData.city}
onChange={handleChange}
fullWidth
required
/>
<TextField
label="Zip Code"
name="zipCode"
value={formData.zipCode}
onChange={handleChange}
fullWidth
required
/>
</Stack>
</Stack>
);
case 2:
return (
<Stack spacing={3}>
<TextField
label="Name on Card"
name="cardName"
value={formData.cardName}
onChange={handleChange}
fullWidth
required
/>
<TextField
label="Card Number"
name="cardNumber"
value={formData.cardNumber}
onChange={handleChange}
fullWidth
required
/>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="Expiration Date"
name="expDate"
placeholder="MM/YY"
value={formData.expDate}
onChange={handleChange}
fullWidth
required
/>
<TextField
label="CVV"
name="cvv"
value={formData.cvv}
onChange={handleChange}
fullWidth
required
/>
</Stack>
</Stack>
);
default:
return 'Unknown step';
}
};
return (
<Paper elevation={3} sx={{ p: 4, maxWidth: 800, mx: 'auto' }}>
<Stack spacing={4} component="form" onSubmit={handleSubmit}>
<Typography variant="h4" component="h1">
Multi-step Form
</Typography>
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{getStepContent(activeStep)}
<Stack direction="row" spacing={2} justifyContent="flex-end">
{activeStep > 0 && (
<Button onClick={handleBack}>
Back
</Button>
)}
<Button
variant="contained"
type="submit"
>
{activeStep === steps.length - 1 ? 'Submit' : 'Next'}
</Button>
</Stack>
</Stack>
</Paper>
);
}
export default MultiStepForm;
This multi-step form uses:
- A Stepper component to show progress
- Different form content for each step
- Consistent Stack layouts across all steps
- Navigation buttons to move between steps
Dynamic Form Fields
Sometimes you need to add or remove form fields dynamically:
import React, { useState } from 'react';
import {
Stack,
TextField,
Button,
Typography,
IconButton,
Paper
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
function DynamicFieldsForm() {
const [education, setEducation] = useState([
{ school: '', degree: '', year: '' }
]);
const handleEducationChange = (index, field, value) => {
const newEducation = [...education];
newEducation[index][field] = value;
setEducation(newEducation);
};
const addEducation = () => {
setEducation([...education, { school: '', degree: '', year: '' }]);
};
const removeEducation = (index) => {
const newEducation = [...education];
newEducation.splice(index, 1);
setEducation(newEducation);
};
const handleSubmit = (event) => {
event.preventDefault();
console.log('Education data:', education);
};
return (
<Paper elevation={3} sx={{ p: 4, maxWidth: 800, mx: 'auto' }}>
<Stack spacing={4} component="form" onSubmit={handleSubmit}>
<Typography variant="h4" component="h1">
Education History
</Typography>
{education.map((edu, index) => (
<Stack
key={index}
spacing={2}
direction={{ xs: 'column', md: 'row' }}
alignItems="flex-start"
>
<Stack spacing={2} sx={{ width: '100%' }}>
<TextField
label="School/University"
value={edu.school}
onChange={(e) => handleEducationChange(index, 'school', e.target.value)}
fullWidth
required
/>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="Degree"
value={edu.degree}
onChange={(e) => handleEducationChange(index, 'degree', e.target.value)}
fullWidth
required
/>
<TextField
label="Year"
value={edu.year}
onChange={(e) => handleEducationChange(index, 'year', e.target.value)}
fullWidth
required
/>
</Stack>
</Stack>
{education.length > 1 && (
<IconButton
color="error"
onClick={() => removeEducation(index)}
sx={{ mt: { xs: 0, md: 2 } }}
>
<DeleteIcon />
</IconButton>
)}
</Stack>
))}
<Button
startIcon={<AddIcon />}
onClick={addEducation}
variant="outlined"
sx={{ alignSelf: 'flex-start' }}
>
Add Education
</Button>
<Button
type="submit"
variant="contained"
sx={{ alignSelf: 'flex-end' }}
>
Submit
</Button>
</Stack>
</Paper>
);
}
export default DynamicFieldsForm;
This form allows users to add multiple education entries. Key points:
- We use state to manage a dynamic array of education entries
- Each entry has its own Stack layout
- Add/remove buttons let users control the number of entries
- We maintain consistent spacing and alignment throughout
Best Practices and Common Issues
Based on my experience, here are some best practices and common issues to watch for when using Stack for form layouts.
Best Practices
-
Be consistent with spacing: Use the same spacing values throughout your form for visual harmony. Typically, I use
spacing={2}
for elements within a section andspacing={3}
orspacing={4}
between sections. -
Use nested Stacks effectively: Don't be afraid to nest Stack components for complex layouts. Just be mindful of the direction prop (column vs row) at each level.
-
Make forms responsive: Always use responsive values for direction and spacing to ensure your forms look good on all devices.
-
Group related fields: Use Stack to visually group related fields together, which improves form usability.
-
Use dividers strategically: Dividers help users understand the form's structure, but don't overuse them.
-
Add visual hierarchy: Use different spacing values and typography to create clear visual hierarchy.
-
Maintain consistent width: Use
fullWidth
on form controls and set amaxWidth
on the main Stack to ensure consistent sizing.
Common Issues and Solutions
-
Issue: Form controls have inconsistent widths Solution: Use
fullWidth
prop on all TextField components and set explicit widths on the Stack.<Stack spacing={2} sx={{ width: '100%', maxWidth: 500 }}> <TextField fullWidth /> <TextField fullWidth /> </Stack>
-
Issue: Stack doesn't fill its container Solution: Add
width: '100%'
to the Stack's sx prop. -
Issue: Alignment issues in responsive layouts Solution: Use the
alignItems
prop to control cross-axis alignment.<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems={{ xs: 'stretch', sm: 'flex-start' }} > <TextField /> <TextField /> </Stack>
-
Issue: Uneven spacing when nesting Stacks Solution: Be mindful of cumulative margins and use the
useFlexGap
prop for more predictable spacing.<Stack spacing={2} useFlexGap> {/* Children here */} </Stack>
-
Issue: Submit button alignment Solution: Use
alignSelf
to position the submit button.<Button type="submit" variant="contained" sx={{ alignSelf: 'flex-end' }} > Submit </Button>
Accessibility Considerations
When building forms with Stack, it's important to ensure they're accessible to all users.
Keyboard Navigation
Ensure your form can be navigated using only the keyboard:
import React from 'react';
import {
Stack,
TextField,
Button,
FormControlLabel,
Checkbox
} from '@mui/material';
function AccessibleForm() {
return (
<Stack
component="form"
spacing={2}
sx={{ width: '100%', maxWidth: 500 }}
noValidate
>
<TextField
id="name-field"
label="Name"
aria-required="true"
fullWidth
/>
<TextField
id="email-field"
label="Email"
type="email"
aria-required="true"
fullWidth
/>
<FormControlLabel
control={
<Checkbox id="terms-checkbox" />
}
label="I accept the terms and conditions"
/>
<Button
variant="contained"
type="submit"
aria-label="Submit form"
>
Submit
</Button>
</Stack>
);
}
export default AccessibleForm;
Key accessibility improvements:
- Added
id
attributes to form elements for proper labeling - Added
aria-required
to required fields - Added
aria-label
to the submit button
Focus Management
For multi-step forms or dynamic forms, proper focus management is crucial:
import React, { useState, useRef } from 'react';
import {
Stack,
TextField,
Button,
Typography
} from '@mui/material';
function FocusManagementForm() {
const [step, setStep] = useState(1);
const firstFieldRef = useRef(null);
const goToNextStep = () => {
setStep(2);
// Focus first field of next step after render
setTimeout(() => {
if (firstFieldRef.current) {
firstFieldRef.current.focus();
}
}, 0);
};
return (
<Stack spacing={3} sx={{ width: '100%', maxWidth: 500 }}>
<Typography variant="h5">
{step === 1 ? 'Step 1: Basic Info' : 'Step 2: Additional Info'}
</Typography>
{step === 1 ? (
<Stack spacing={2}>
<TextField
label="Name"
fullWidth
autoFocus
/>
<TextField
label="Email"
type="email"
fullWidth
/>
<Button onClick={goToNextStep} variant="contained">
Next
</Button>
</Stack>
) : (
<Stack spacing={2}>
<TextField
label="Phone"
fullWidth
inputRef={firstFieldRef}
/>
<TextField
label="Address"
fullWidth
/>
<Button type="submit" variant="contained">
Submit
</Button>
</Stack>
)}
</Stack>
);
}
export default FocusManagementForm;
This example demonstrates:
- Using
autoFocus
on the first field of the initial step - Using
inputRef
and focus management when moving between steps - Proper heading structure to indicate the current step
Performance Optimization
For large forms, performance can become an issue. Here are some tips for optimizing form performance with Stack:
import React, { useState, useMemo } from 'react';
import {
Stack,
TextField,
Button,
Typography
} from '@mui/material';
function OptimizedForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
// Memoize static parts of the form
const formHeader = useMemo(() => (
<Typography variant="h5" component="h2">
Contact Form
</Typography>
), []);
// Use callback for input changes
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
return (
<Stack
component="form"
spacing={3}
sx={{ width: '100%', maxWidth: 500 }}
>
{formHeader}
<TextField
label="Name"
name="name"
value={formData.name}
onChange={handleChange}
fullWidth
/>
<TextField
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
fullWidth
/>
<TextField
label="Message"
name="message"
multiline
rows={4}
value={formData.message}
onChange={handleChange}
fullWidth
/>
<Button
type="submit"
variant="contained"
>
Submit
</Button>
</Stack>
);
}
export default OptimizedForm;
Performance optimization techniques:
- Using
useMemo
for static parts of the form - Using a single change handler for all fields
- Keeping the component structure simple
For very large forms, consider splitting them into smaller components or using virtualization for fields that aren't immediately visible.
Wrapping Up
The Stack component is a powerful tool for creating clean, maintainable form layouts in Material UI. By leveraging its flexible spacing system and responsive capabilities, you can build forms that look professional and adapt seamlessly to different screen sizes.
Throughout this guide, we've explored how to use Stack for basic and advanced form layouts, integrate it with form libraries, handle dynamic content, and ensure accessibility. By following these patterns and best practices, you can create forms that are both visually appealing and highly functional.
Remember that good form design is about more than just layout—it's about creating an intuitive user experience. Stack helps you achieve this by providing a solid foundation for your form's structure, allowing you to focus on the details that make your forms truly effective.