Menu

Building Reusable Form Actions with MUI Button: A Complete Guide to Theme Overrides and Patterns

As a front-end developer, I've spent countless hours refactoring the same button components across different projects. One pattern I've found particularly valuable is creating reusable form action buttons with Material UI. In this guide, I'll show you how to leverage MUI Button's flexibility to create a reusable pattern for form actions that's both maintainable and themeable.

By the end of this article, you'll have a robust, reusable button system for your forms that will save you time and ensure consistency across your application.

What You'll Learn

In this comprehensive guide, you'll learn:

  • How to analyze MUI Button's API to build extensible components
  • Creating a reusable FormActionButton component with proper prop handling
  • Implementing theme overrides for consistent styling across your application
  • Building button variants for different form actions (submit, cancel, reset)
  • Handling loading states, disabled states, and accessibility
  • Integrating with form libraries like Formik and React Hook Form
  • Advanced patterns for conditional rendering and composition

Understanding MUI Button: A Deep Dive

Before we start building our reusable pattern, let's understand the MUI Button component thoroughly. This will help us make informed decisions about our implementation.

Button Variants and Properties

MUI Button comes with three main variants: contained, outlined, and text. Each variant has its own visual style and is suitable for different situations in your UI.


// Contained buttons (high emphasis)
<Button variant="contained">Submit</Button>

// Outlined buttons (medium emphasis)
<Button variant="outlined">Cancel</Button>

// Text buttons (low emphasis)
<Button variant="text">Reset</Button>

The Button component accepts numerous props that control its appearance and behavior. Here are the most important ones:

PropTypeDefaultDescription
childrennode-The content of the button
color'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning' | string'primary'The color of the button
disabledbooleanfalseIf true, the button will be disabled
disableElevationbooleanfalseIf true, no elevation is used
endIconnode-Element placed after the children
fullWidthbooleanfalseIf true, the button will take up the full width of its container
hrefstring-The URL to link to when the button is clicked
size'small' | 'medium' | 'large''medium'The size of the button
startIconnode-Element placed before the children
type'button' | 'submit' | 'reset''button'The type of button
variant'contained' | 'outlined' | 'text''text'The variant to use

Styling and Customization Options

MUI Button can be styled in multiple ways:

  1. Using the sx prop: For one-off styling needs
  2. Theme customization: For app-wide button styling
  3. Styled API: For creating styled components based on Button

Here's a quick example of using the sx prop:


<Button
  variant="contained"
  sx={{
    backgroundColor: 'darkblue',
    '&:hover': {
      backgroundColor: 'navy',
    },
    borderRadius: 2,
    textTransform: 'none',
  }}
>
  Custom Button
</Button>

For theme customization, you'd modify your theme like this:


const theme = createTheme({
  components: {
    MuiButton: {
      styleOverrides: {
        root: {
          textTransform: 'none',
          borderRadius: 8,
        },
        contained: {
          boxShadow: 'none',
          '&:hover': {
            boxShadow: 'none',
          },
        },
      },
    },
  },
});

Accessibility Considerations

MUI Button is designed with accessibility in mind, but there are still important considerations:

  • Always provide meaningful text for screen readers
  • Use appropriate color contrast
  • Ensure keyboard navigation works correctly
  • Use proper ARIA attributes when necessary

For example, if your button only contains an icon, you should provide an aria-label:


<Button 
  aria-label="Add to cart"
  startIcon={<ShoppingCartIcon />}
>
  Add to cart
</Button>

Building a Reusable Form Action Button

Now that we understand the MUI Button component, let's create our reusable FormActionButton component. We'll start with a basic implementation and then enhance it.

Step 1: Create the Base Component

First, let's create a base component that extends the MUI Button with form-specific functionality.


import React from 'react';
import { Button, CircularProgress } from '@mui/material';
import PropTypes from 'prop-types';

const FormActionButton = ({
  children,
  loading = false,
  actionType = 'submit',
  variant = 'contained',
  color = 'primary',
  fullWidth = false,
  disabled = false,
  onClick,
  ...props
}) => {
  // Map actionType to button type and variant
  const buttonTypes = {
    submit: { type: 'submit', defaultVariant: 'contained', defaultColor: 'primary' },
    reset: { type: 'reset', defaultVariant: 'outlined', defaultColor: 'secondary' },
    cancel: { type: 'button', defaultVariant: 'text', defaultColor: 'inherit' },
  };

  const { type, defaultVariant, defaultColor } = buttonTypes[actionType] || buttonTypes.submit;
  
  // Use provided variant/color or default based on actionType
  const buttonVariant = variant || defaultVariant;
  const buttonColor = color || defaultColor;

  return (
    <Button
      type={type}
      variant={buttonVariant}
      color={buttonColor}
      fullWidth={fullWidth}
      disabled={disabled || loading}
      onClick={onClick}
      startIcon={loading ? <CircularProgress size={20} color="inherit" /> : null}
      {...props}
    >
      {children}
    </Button>
  );
};

FormActionButton.propTypes = {
  children: PropTypes.node.isRequired,
  loading: PropTypes.bool,
  actionType: PropTypes.oneOf(['submit', 'reset', 'cancel']),
  variant: PropTypes.oneOf(['contained', 'outlined', 'text']),
  color: PropTypes.string,
  fullWidth: PropTypes.bool,
  disabled: PropTypes.bool,
  onClick: PropTypes.func,
};

export default FormActionButton;

This base component provides several advantages:

  1. Action Type Mapping: We map actionType to appropriate button types and default styling
  2. Loading State: We handle loading states with a spinner
  3. Sensible Defaults: We provide reasonable defaults based on the action type
  4. Prop Forwarding: We forward all other props to the underlying Button component

Step 2: Create Specialized Button Components

Now, let's create specialized buttons for common form actions. This will make our form code more readable and consistent.


import React from 'react';
import FormActionButton from './FormActionButton';

export const SubmitButton = (props) => (
  <FormActionButton actionType="submit" {...props} />
);

export const ResetButton = (props) => (
  <FormActionButton actionType="reset" {...props} />
);

export const CancelButton = (props) => (
  <FormActionButton actionType="cancel" {...props} />
);

Step 3: Add Theme Customization

To ensure consistent styling across our application, let's add theme customization for our form action buttons.


import { createTheme } from '@mui/material/styles';

const theme = createTheme({
  components: {
    MuiButton: {
      styleOverrides: {
        // Base styles for all buttons
        root: {
          textTransform: 'none',
          borderRadius: 8,
          padding: '8px 24px',
          fontWeight: 600,
        },
        // Styles for contained buttons
        contained: {
          boxShadow: 'none',
          '&:hover': {
            boxShadow: 'none',
          },
        },
        // Styles for outlined buttons
        outlined: {
          borderWidth: 2,
          '&:hover': {
            borderWidth: 2,
          },
        },
      },
      // Default props for all buttons
      defaultProps: {
        disableElevation: true,
      },
      // Variant-specific styles
      variants: [
        {
          props: { variant: 'contained', color: 'primary' },
          style: {
            backgroundColor: '#1976d2',
            '&:hover': {
              backgroundColor: '#1565c0',
            },
          },
        },
        {
          props: { variant: 'contained', color: 'secondary' },
          style: {
            backgroundColor: '#9c27b0',
            '&:hover': {
              backgroundColor: '#7b1fa2',
            },
          },
        },
        {
          props: { actionType: 'submit' },
          style: {
            minWidth: 120,
          },
        },
        {
          props: { actionType: 'cancel' },
          style: {
            color: '#666',
          },
        },
      ],
    },
  },
});

export default theme;

Step 4: Create a Custom Theme Provider for Form Actions

To make our theme customization more focused, let's create a dedicated theme provider for form actions.


import React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { useTheme } from '@mui/material/styles';

const FormActionThemeProvider = ({ children }) => {
  // Get the parent theme
  const parentTheme = useTheme();
  
  // Create a new theme that extends the parent theme
  const formActionTheme = createTheme({
    ...parentTheme,
    components: {
      ...parentTheme.components,
      MuiButton: {
        ...parentTheme.components?.MuiButton,
        styleOverrides: {
          ...parentTheme.components?.MuiButton?.styleOverrides,
          // Form action specific overrides
          root: {
            ...parentTheme.components?.MuiButton?.styleOverrides?.root,
            fontSize: '0.9rem',
            transition: 'all 0.2s ease-in-out',
          },
        },
        variants: [
          ...(parentTheme.components?.MuiButton?.variants || []),
          {
            props: { actionType: 'submit' },
            style: {
              minWidth: 120,
              fontWeight: 600,
            },
          },
          {
            props: { actionType: 'reset' },
            style: {
              minWidth: 100,
            },
          },
          {
            props: { actionType: 'cancel' },
            style: {
              minWidth: 100,
              color: parentTheme.palette.text.secondary,
            },
          },
        ],
      },
    },
  });

  return <ThemeProvider theme={formActionTheme}>{children}</ThemeProvider>;
};

export default FormActionThemeProvider;

Step-by-Step Implementation Guide

Now let's walk through a complete implementation of our form action button pattern in a real-world application.

Step 1: Set Up Your Project

First, make sure you have the necessary dependencies installed:


npm install @mui/material @mui/icons-material @emotion/react @emotion/styled prop-types

Step 2: Create the Theme Configuration

Create a file called theme.js with your theme configuration:


// src/theme.js
import { createTheme } from '@mui/material/styles';

const theme = createTheme({
  palette: {
    primary: {
      main: '#2196f3',
      light: '#64b5f6',
      dark: '#1976d2',
      contrastText: '#fff',
    },
    secondary: {
      main: '#f50057',
      light: '#ff4081',
      dark: '#c51162',
      contrastText: '#fff',
    },
    success: {
      main: '#4caf50',
      light: '#81c784',
      dark: '#388e3c',
      contrastText: '#fff',
    },
    error: {
      main: '#f44336',
      light: '#e57373',
      dark: '#d32f2f',
      contrastText: '#fff',
    },
  },
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
    button: {
      textTransform: 'none',
      fontWeight: 500,
    },
  },
  components: {
    MuiButton: {
      styleOverrides: {
        root: {
          borderRadius: 8,
          padding: '8px 16px',
        },
        contained: {
          boxShadow: 'none',
          '&:hover': {
            boxShadow: 'none',
          },
        },
      },
      defaultProps: {
        disableElevation: true,
      },
    },
  },
});

export default theme;

Step 3: Create the FormActionButton Component

Create a file called FormActionButton.js:


// src/components/FormActionButton.js
import React from 'react';
import { Button, CircularProgress } from '@mui/material';
import PropTypes from 'prop-types';

const FormActionButton = ({
  children,
  loading = false,
  actionType = 'submit',
  variant,
  color,
  size = 'medium',
  fullWidth = false,
  disabled = false,
  onClick,
  startIcon,
  endIcon,
  sx = {},
  ...props
}) => {
  // Map actionType to button type and default styling
  const actionTypeConfig = {
    submit: {
      type: 'submit',
      defaultVariant: 'contained',
      defaultColor: 'primary',
      loadingText: 'Submitting...',
    },
    reset: {
      type: 'reset',
      defaultVariant: 'outlined',
      defaultColor: 'secondary',
      loadingText: 'Resetting...',
    },
    cancel: {
      type: 'button',
      defaultVariant: 'text',
      defaultColor: 'inherit',
      loadingText: 'Cancelling...',
    },
    delete: {
      type: 'button',
      defaultVariant: 'contained',
      defaultColor: 'error',
      loadingText: 'Deleting...',
    },
  };

  const config = actionTypeConfig[actionType] || actionTypeConfig.submit;
  
  // Use provided variant/color or default based on actionType
  const buttonType = config.type;
  const buttonVariant = variant || config.defaultVariant;
  const buttonColor = color || config.defaultColor;
  
  // Handle loading state
  const buttonText = loading && props.loadingText ? props.loadingText : children;
  const buttonStartIcon = loading ? (
    <CircularProgress size={20} color="inherit" />
  ) : (
    startIcon
  );

  // Merge sx props
  const defaultSx = {
    position: 'relative',
    ...(actionType === 'submit' && { minWidth: 120 }),
    ...(actionType === 'cancel' && { minWidth: 100 }),
  };

  return (
    <Button
      type={buttonType}
      variant={buttonVariant}
      color={buttonColor}
      size={size}
      fullWidth={fullWidth}
      disabled={disabled || loading}
      onClick={onClick}
      startIcon={buttonStartIcon}
      endIcon={loading ? null : endIcon}
      sx={{ ...defaultSx, ...sx }}
      {...props}
      data-action-type={actionType}
    >
      {buttonText}
    </Button>
  );
};

FormActionButton.propTypes = {
  children: PropTypes.node.isRequired,
  loading: PropTypes.bool,
  loadingText: PropTypes.string,
  actionType: PropTypes.oneOf(['submit', 'reset', 'cancel', 'delete']),
  variant: PropTypes.oneOf(['contained', 'outlined', 'text']),
  color: PropTypes.string,
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  fullWidth: PropTypes.bool,
  disabled: PropTypes.bool,
  onClick: PropTypes.func,
  startIcon: PropTypes.node,
  endIcon: PropTypes.node,
  sx: PropTypes.object,
};

export default FormActionButton;

Step 4: Create Specialized Button Components

Create a file called FormButtons.js:


// src/components/FormButtons.js
import React from 'react';
import FormActionButton from './FormActionButton';
import PropTypes from 'prop-types';
import { 
  Save as SaveIcon, 
  Cancel as CancelIcon, 
  Refresh as RefreshIcon,
  Delete as DeleteIcon
} from '@mui/icons-material';

export const SubmitButton = ({ children = 'Submit', startIcon = <SaveIcon />, ...props }) => (
  <FormActionButton 
    actionType="submit" 
    startIcon={startIcon}
    {...props}
  >
    {children}
  </FormActionButton>
);

export const ResetButton = ({ children = 'Reset', startIcon = <RefreshIcon />, ...props }) => (
  <FormActionButton 
    actionType="reset" 
    startIcon={startIcon}
    {...props}
  >
    {children}
  </FormActionButton>
);

export const CancelButton = ({ children = 'Cancel', startIcon = <CancelIcon />, ...props }) => (
  <FormActionButton 
    actionType="cancel" 
    startIcon={startIcon}
    {...props}
  >
    {children}
  </FormActionButton>
);

export const DeleteButton = ({ children = 'Delete', startIcon = <DeleteIcon />, ...props }) => (
  <FormActionButton 
    actionType="delete" 
    startIcon={startIcon}
    {...props}
  >
    {children}
  </FormActionButton>
);

// Common PropTypes for all specialized buttons
const buttonPropTypes = {
  children: PropTypes.node,
  loading: PropTypes.bool,
  loadingText: PropTypes.string,
  variant: PropTypes.oneOf(['contained', 'outlined', 'text']),
  color: PropTypes.string,
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  fullWidth: PropTypes.bool,
  disabled: PropTypes.bool,
  onClick: PropTypes.func,
  startIcon: PropTypes.node,
  endIcon: PropTypes.node,
  sx: PropTypes.object,
};

SubmitButton.propTypes = buttonPropTypes;
ResetButton.propTypes = buttonPropTypes;
CancelButton.propTypes = buttonPropTypes;
DeleteButton.propTypes = buttonPropTypes;

Step 5: Create a Form Actions Container Component

To organize form buttons consistently, let's create a container component:


// src/components/FormActions.js
import React from 'react';
import { Box } from '@mui/material';
import PropTypes from 'prop-types';

const FormActions = ({ 
  children, 
  spacing = 2, 
  direction = 'row', 
  justifyContent = 'flex-end',
  sx = {},
  ...props 
}) => {
  return (
    <Box
      sx={{
        display: 'flex',
        flexDirection: direction,
        justifyContent,
        gap: spacing,
        mt: 3,
        ...sx,
      }}
      {...props}
    >
      {children}
    </Box>
  );
};

FormActions.propTypes = {
  children: PropTypes.node.isRequired,
  spacing: PropTypes.number,
  direction: PropTypes.oneOf(['row', 'column', 'row-reverse', 'column-reverse']),
  justifyContent: PropTypes.string,
  sx: PropTypes.object,
};

export default FormActions;

Step 6: Create Theme Overrides for Form Actions

Now, let's create a dedicated theme provider for form actions:


// src/components/FormActionThemeProvider.js
import React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { useTheme } from '@mui/material/styles';
import PropTypes from 'prop-types';

const FormActionThemeProvider = ({ children, customOverrides = {} }) => {
  // Get the parent theme
  const parentTheme = useTheme();
  
  // Create a new theme that extends the parent theme
  const formActionTheme = createTheme({
    ...parentTheme,
    components: {
      ...parentTheme.components,
      MuiButton: {
        ...parentTheme.components?.MuiButton,
        styleOverrides: {
          ...parentTheme.components?.MuiButton?.styleOverrides,
          // Form action specific overrides
          root: {
            ...parentTheme.components?.MuiButton?.styleOverrides?.root,
            fontSize: '0.9rem',
            transition: 'all 0.2s ease-in-out',
            ...customOverrides.root,
          },
          // Variant specific overrides
          contained: {
            ...parentTheme.components?.MuiButton?.styleOverrides?.contained,
            ...customOverrides.contained,
          },
          outlined: {
            ...parentTheme.components?.MuiButton?.styleOverrides?.outlined,
            ...customOverrides.outlined,
          },
          text: {
            ...parentTheme.components?.MuiButton?.styleOverrides?.text,
            ...customOverrides.text,
          },
        },
        variants: [
          ...(parentTheme.components?.MuiButton?.variants || []),
          // Action type specific styling
          {
            props: { 'data-action-type': 'submit' },
            style: {
              minWidth: 120,
              fontWeight: 600,
              ...customOverrides.submit,
            },
          },
          {
            props: { 'data-action-type': 'reset' },
            style: {
              minWidth: 100,
              ...customOverrides.reset,
            },
          },
          {
            props: { 'data-action-type': 'cancel' },
            style: {
              minWidth: 100,
              color: parentTheme.palette.text.secondary,
              ...customOverrides.cancel,
            },
          },
          {
            props: { 'data-action-type': 'delete' },
            style: {
              minWidth: 120,
              ...customOverrides.delete,
            },
          },
        ],
      },
    },
  });

  return <ThemeProvider theme={formActionTheme}>{children}</ThemeProvider>;
};

FormActionThemeProvider.propTypes = {
  children: PropTypes.node.isRequired,
  customOverrides: PropTypes.object,
};

export default FormActionThemeProvider;

Step 7: Use the Components in a Form

Now, let's put everything together in a form:


// src/components/UserForm.js
import React, { useState } from 'react';
import { 
  Box, 
  TextField, 
  Typography, 
  Paper,
  Divider
} from '@mui/material';
import FormActions from './FormActions';
import { SubmitButton, ResetButton, CancelButton } from './FormButtons';
import FormActionThemeProvider from './FormActionThemeProvider';

const UserForm = ({ onSubmit, onCancel, initialData = {}, isEditMode = false }) => {
  const [formData, setFormData] = useState({
    firstName: initialData.firstName || '',
    lastName: initialData.lastName || '',
    email: initialData.email || '',
    ...initialData
  });
  const [loading, setLoading] = useState(false);
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 1500));
      if (onSubmit) {
        await onSubmit(formData);
      }
    } catch (error) {
      console.error('Form submission error:', error);
    } finally {
      setLoading(false);
    }
  };
  
  const handleReset = () => {
    setFormData({
      firstName: initialData.firstName || '',
      lastName: initialData.lastName || '',
      email: initialData.email || '',
      ...initialData
    });
  };
  
  return (
    <Paper 
      elevation={2} 
      sx={{ 
        p: 3, 
        maxWidth: 600, 
        mx: 'auto', 
        mt: 4 
      }}
    >
      <Typography variant="h5" gutterBottom>
        {isEditMode ? 'Edit User' : 'Create New User'}
      </Typography>
      <Divider sx={{ mb: 3 }} />
      
      <Box component="form" onSubmit={handleSubmit} noValidate>
        <TextField
          margin="normal"
          required
          fullWidth
          id="firstName"
          label="First Name"
          name="firstName"
          value={formData.firstName}
          onChange={handleChange}
          autoFocus
        />
        <TextField
          margin="normal"
          required
          fullWidth
          id="lastName"
          label="Last Name"
          name="lastName"
          value={formData.lastName}
          onChange={handleChange}
        />
        <TextField
          margin="normal"
          required
          fullWidth
          id="email"
          label="Email Address"
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
        />
        
        <FormActionThemeProvider
          customOverrides={{
            submit: {
              fontWeight: 700,
            },
            cancel: {
              fontStyle: 'italic',
            }
          }}
        >
          <FormActions>
            <CancelButton 
              onClick={onCancel} 
              disabled={loading}
            />
            <ResetButton 
              onClick={handleReset} 
              disabled={loading}
            />
            <SubmitButton 
              loading={loading} 
              loadingText="Saving..."
            >
              {isEditMode ? 'Update User' : 'Create User'}
            </SubmitButton>
          </FormActions>
        </FormActionThemeProvider>
      </Box>
    </Paper>
  );
};

export default UserForm;

Step 8: Use the Form in Your Application

Finally, use the form in your application:


// src/App.js
import React from 'react';
import { ThemeProvider, CssBaseline, Container, Box } from '@mui/material';
import theme from './theme';
import UserForm from './components/UserForm';

function App() {
  const handleSubmit = (data) => {
    console.log('Form submitted with:', data);
    alert('Form submitted successfully!');
  };
  
  const handleCancel = () => {
    console.log('Form cancelled');
    alert('Form cancelled');
  };
  
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Container>
        <Box sx={{ my: 4 }}>
          <UserForm 
            onSubmit={handleSubmit} 
            onCancel={handleCancel} 
            initialData={{
              firstName: 'John',
              lastName: 'Doe',
              email: 'john.doe@example.com'
            }}
            isEditMode={true}
          />
        </Box>
      </Container>
    </ThemeProvider>
  );
}

export default App;

Advanced Form Action Button Patterns

Now that we have a solid foundation, let's explore some advanced patterns for our form action buttons.

Integration with Form Libraries

Our FormActionButton works great with form libraries like Formik and React Hook Form. Here's an example with Formik:


// src/components/FormikUserForm.js
import React from 'react';
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
import { 
  Box, 
  Typography, 
  Paper,
  Divider
} from '@mui/material';
import { TextField } from 'formik-mui';
import FormActions from './FormActions';
import { SubmitButton, ResetButton, CancelButton } from './FormButtons';
import FormActionThemeProvider from './FormActionThemeProvider';

// Validation schema
const UserSchema = 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'),
});

const FormikUserForm = ({ onSubmit, onCancel, initialValues = {}, isEditMode = false }) => {
  const defaultValues = {
    firstName: '',
    lastName: '',
    email: '',
    ...initialValues
  };
  
  return (
    <Paper elevation={2} sx={{ p: 3, maxWidth: 600, mx: 'auto', mt: 4 }}>
      <Typography variant="h5" gutterBottom>
        {isEditMode ? 'Edit User' : 'Create New User'}
      </Typography>
      <Divider sx={{ mb: 3 }} />
      
      <Formik
        initialValues={defaultValues}
        validationSchema={UserSchema}
        onSubmit={async (values, { setSubmitting }) => {
          // Simulate API call
          await new Promise(resolve => setTimeout(resolve, 1500));
          if (onSubmit) {
            await onSubmit(values);
          }
          setSubmitting(false);
        }}
      >
        {({ isSubmitting, resetForm, dirty, isValid }) => (
          <Form>
            <Field
              component={TextField}
              name="firstName"
              label="First Name"
              fullWidth
              margin="normal"
              required
            />
            <Field
              component={TextField}
              name="lastName"
              label="Last Name"
              fullWidth
              margin="normal"
              required
            />
            <Field
              component={TextField}
              name="email"
              type="email"
              label="Email Address"
              fullWidth
              margin="normal"
              required
            />
            
            <FormActionThemeProvider>
              <FormActions>
                <CancelButton 
                  onClick={onCancel} 
                  disabled={isSubmitting}
                />
                <ResetButton 
                  onClick={() => resetForm()} 
                  disabled={isSubmitting || !dirty}
                />
                <SubmitButton 
                  loading={isSubmitting} 
                  loadingText="Saving..."
                  disabled={!dirty || !isValid}
                >
                  {isEditMode ? 'Update User' : 'Create User'}
                </SubmitButton>
              </FormActions>
            </FormActionThemeProvider>
          </Form>
        )}
      </Formik>
    </Paper>
  );
};

export default FormikUserForm;

Conditional Rendering Based on Form State

We can enhance our buttons to respond to form state changes:


// src/components/SmartFormActions.js
import React from 'react';
import { useFormikContext } from 'formik';
import FormActions from './FormActions';
import { SubmitButton, ResetButton, CancelButton } from './FormButtons';
import FormActionThemeProvider from './FormActionThemeProvider';

const SmartFormActions = ({ 
  onCancel, 
  showReset = true, 
  showCancel = true,
  submitText,
  resetText,
  cancelText,
  direction = 'row',
  spacing = 2,
  ...props 
}) => {
  const { isSubmitting, dirty, isValid, resetForm } = useFormikContext();
  
  return (
    <FormActionThemeProvider>
      <FormActions direction={direction} spacing={spacing} {...props}>
        {showCancel && (
          <CancelButton 
            onClick={onCancel} 
            disabled={isSubmitting}
          >
            {cancelText || 'Cancel'}
          </CancelButton>
        )}
        
        {showReset && (
          <ResetButton 
            onClick={() => resetForm()} 
            disabled={isSubmitting || !dirty}
          >
            {resetText || 'Reset'}
          </ResetButton>
        )}
        
        <SubmitButton 
          loading={isSubmitting} 
          loadingText="Saving..."
          disabled={!dirty || !isValid}
        >
          {submitText || 'Submit'}
        </SubmitButton>
      </FormActions>
    </FormActionThemeProvider>
  );
};

export default SmartFormActions;

And use it in our form:


// Usage in a Formik form
<Formik
  initialValues={initialValues}
  validationSchema={validationSchema}
  onSubmit={handleSubmit}
>
  <Form>
    {/* Form fields */}
    <SmartFormActions 
      onCancel={handleCancel} 
      submitText={isEditMode ? 'Update User' : 'Create User'} 
    />
  </Form>
</Formik>

Creating Button Groups for Common Form Patterns

For even more reusability, let's create button groups for common form patterns:


// src/components/FormButtonGroups.js
import React from 'react';
import FormActions from './FormActions';
import { SubmitButton, ResetButton, CancelButton, DeleteButton } from './FormButtons';
import FormActionThemeProvider from './FormActionThemeProvider';

// Standard form buttons: Submit, Reset, Cancel
export const StandardFormButtons = ({ 
  onCancel, 
  submitText = 'Submit', 
  resetText = 'Reset',
  cancelText = 'Cancel',
  isSubmitting = false,
  isDirty = true,
  isValid = true,
  onReset,
  ...props 
}) => (
  <FormActionThemeProvider>
    <FormActions {...props}>
      <CancelButton 
        onClick={onCancel} 
        disabled={isSubmitting}
      >
        {cancelText}
      </CancelButton>
      <ResetButton 
        onClick={onReset} 
        disabled={isSubmitting || !isDirty}
      >
        {resetText}
      </ResetButton>
      <SubmitButton 
        loading={isSubmitting} 
        loadingText="Saving..."
        disabled={!isDirty || !isValid}
      >
        {submitText}
      </SubmitButton>
    </FormActions>
  </FormActionThemeProvider>
);

// Edit form buttons: Submit, Cancel, Delete
export const EditFormButtons = ({ 
  onCancel, 
  onDelete,
  submitText = 'Save Changes', 
  cancelText = 'Cancel',
  deleteText = 'Delete',
  isSubmitting = false,
  isDeleting = false,
  isDirty = true,
  isValid = true,
  ...props 
}) => (
  <FormActionThemeProvider>
    <FormActions {...props}>
      <CancelButton 
        onClick={onCancel} 
        disabled={isSubmitting || isDeleting}
      >
        {cancelText}
      </CancelButton>
      <DeleteButton 
        onClick={onDelete} 
        loading={isDeleting}
        loadingText="Deleting..."
        disabled={isSubmitting}
      >
        {deleteText}
      </DeleteButton>
      <SubmitButton 
        loading={isSubmitting} 
        loadingText="Saving..."
        disabled={!isDirty || !isValid || isDeleting}
      >
        {submitText}
      </SubmitButton>
    </FormActions>
  </FormActionThemeProvider>
);

// Simple form buttons: Submit, Cancel
export const SimpleFormButtons = ({ 
  onCancel, 
  submitText = 'Submit', 
  cancelText = 'Cancel',
  isSubmitting = false,
  isDirty = true,
  isValid = true,
  ...props 
}) => (
  <FormActionThemeProvider>
    <FormActions {...props}>
      <CancelButton 
        onClick={onCancel} 
        disabled={isSubmitting}
      >
        {cancelText}
      </CancelButton>
      <SubmitButton 
        loading={isSubmitting} 
        loadingText="Saving..."
        disabled={!isDirty || !isValid}
      >
        {submitText}
      </SubmitButton>
    </FormActions>
  </FormActionThemeProvider>
);

Best Practices and Common Issues

Here are some best practices and common issues you might encounter when implementing this pattern:

Best Practices

  1. Consistent Button Ordering: Always maintain the same button order across your forms for consistency. A common pattern is Cancel → Reset → Submit (from left to right).

  2. Visual Hierarchy: Use visual styling to emphasize the primary action (usually submit) and de-emphasize secondary actions (cancel, reset).

  3. Loading States: Always provide visual feedback during asynchronous operations by showing loading indicators.

  4. Disable During Submission: Disable all buttons during form submission to prevent multiple submissions.

  5. Responsive Design: Ensure your form actions work well on mobile by using responsive layouts:


<FormActions 
  direction={{ xs: 'column', sm: 'row' }}
  sx={{ 
    '& > button': { 
      width: { xs: '100%', sm: 'auto' } 
    } 
  }}
>
  {/* Buttons */}
</FormActions>
  1. Accessibility: Ensure your buttons have proper ARIA attributes and keyboard navigation:

<SubmitButton
  aria-label="Submit form"
  loading={isSubmitting}
>
  Submit
</SubmitButton>

Common Issues and Solutions

  1. Issue: Buttons not updating when form state changes. Solution: Make sure your buttons are inside the form component context and have access to the form state.

  2. Issue: Theme overrides not being applied. Solution: Ensure your FormActionThemeProvider is properly nested and that the data attributes are correctly set.

  3. Issue: Inconsistent button sizes across forms. Solution: Use the minWidth property in your theme overrides to ensure consistent button widths.

  4. Issue: Loading state not visible on small buttons. Solution: Adjust the size of the CircularProgress component and consider using a minimum width for buttons.

  5. Issue: Buttons wrapping or overflowing on small screens. Solution: Use responsive direction and width properties for your FormActions component.

Performance Considerations

When implementing form action buttons, consider these performance optimizations:

  1. Memoize Button Components: Use React.memo to prevent unnecessary re-renders:

const SubmitButton = React.memo(({ children = 'Submit', ...props }) => (
  <FormActionButton actionType="submit" {...props}>
    {children}
  </FormActionButton>
));
  1. Debounce Submit Handlers: For forms with expensive validation or submission logic, debounce the submit handler:

import { debounce } from 'lodash';

// Inside your component
const debouncedSubmit = React.useMemo(
  () => debounce((values) => {
    // Submit logic here
    console.log('Submitting:', values);
  }, 300),
  []
);
  1. Lazy Load Non-Critical Components: If your form has multiple sections or complex button logic, consider lazy loading:

const DeleteConfirmationDialog = React.lazy(() => 
  import('./DeleteConfirmationDialog')
);

// Then in your component
{showDeleteConfirmation && (
  <React.Suspense fallback={<CircularProgress />}>
    <DeleteConfirmationDialog 
      onConfirm={handleDelete} 
      onCancel={() => setShowDeleteConfirmation(false)} 
    />
  </React.Suspense>
)}

Wrapping Up

In this guide, we've built a comprehensive reusable pattern for form actions using MUI Button. We've covered creating specialized button components, theme customization, integration with form libraries, and advanced patterns for different form scenarios.

By implementing this pattern, you'll achieve:

  • Consistent styling and behavior across all your forms
  • Reduced boilerplate code for common form actions
  • Better maintainability through centralized theme configuration
  • Enhanced user experience with proper loading states and visual feedback
  • Improved accessibility for all users

The approach we've taken is flexible and can be extended to support additional requirements as your application grows. By centralizing your form action button logic, you'll make it easier to implement global changes to your UI and ensure a consistent experience for your users.