Menu

Building a Preference Selector with React MUI Radio Group: A Complete Guide

As a front-end developer working with React and Material UI, you'll often need to create form elements that allow users to select preferences. The MUI Radio Group component is a powerful tool for building such selectors with a clean, accessible interface. In this comprehensive guide, I'll walk you through building a preference selector using MUI's Radio Group with controlled state management.

What You'll Learn

By the end of this article, you'll know how to:

  • Implement a fully controlled Radio Group component in React
  • Structure and organize radio options effectively
  • Handle state changes and form submissions
  • Style and customize your Radio Group for different use cases
  • Integrate with form libraries and validation
  • Solve common implementation challenges
  • Apply accessibility best practices

Understanding MUI Radio Group: The Complete Picture

Before diving into implementation, let's understand what makes the Radio Group component so useful. The Radio Group is a wrapper component that provides context for individual Radio components, managing their selection state and ensuring only one option can be selected at a time.

The Radio Group component works together with FormControl, FormLabel, FormControlLabel, and Radio components to create a complete form element. This modular approach gives you flexibility while maintaining a consistent API and appearance.

Core Components in MUI Radio Group

When building a preference selector with MUI's Radio Group, you'll typically use these components:

  1. FormControl: The container component that provides context to form elements
  2. RadioGroup: Manages the selection state of child Radio components
  3. FormControlLabel: Wraps a Radio component with a label
  4. Radio: The actual radio button element
  5. FormHelperText: Optional text for providing additional guidance or error messages

Let's look at the essential props and features of each component:

RadioGroup Props

PropTypeDefaultDescription
valueany-The value of the selected radio button
onChangefunc-Callback fired when a radio button is selected
namestring-The name used for all radio inputs
rowboolfalseIf true, the radio buttons will be arranged horizontally
defaultValueany-The default value (for uncontrolled component)

Radio Props

PropTypeDefaultDescription
checkedbool-If true, the component is checked
color'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' | string'primary'The color of the component
disabledboolfalseIf true, the radio will be disabled
size'small' | 'medium''medium'The size of the component
valueany-The value of the component

FormControlLabel Props

PropTypeDefaultDescription
controlelement-A control element (e.g., Radio)
labelnode-The text or element to be used as the label
disabledboolfalseIf true, the control and label will be disabled
labelPlacement'end' | 'start' | 'top' | 'bottom''end'The position of the label
valueany-The value of the component

Controlled vs Uncontrolled Usage

When working with MUI Radio Group, you have two approaches for managing state:

Controlled Component

In a controlled component, you explicitly manage the component's state through React state. You provide:

  • A value prop that reflects the current selection
  • An onChange handler that updates the state when a selection changes

This approach gives you full control over the component's behavior and allows you to easily integrate with other parts of your application.

Uncontrolled Component

With an uncontrolled component, you let the DOM handle the form state internally. You provide:

  • A defaultValue prop for the initial selection
  • A name prop for form submission

For most professional applications, the controlled approach is recommended as it provides more predictable behavior and better integration with React's state management.

Creating a Basic Preference Selector

Let's start by building a simple preference selector using the Radio Group component. We'll create a component that allows users to select their preferred theme (light, dark, or system).

Step 1: Set Up Your Project

First, make sure you have MUI installed in your React project:

npm install @mui/material @emotion/react @emotion/styled

Step 2: Create the Basic Preference Selector Component

Let's create a ThemePreferenceSelector component:

import React, { useState } from 'react';
import {
  FormControl,
  FormLabel,
  RadioGroup,
  FormControlLabel,
  Radio,
  Paper,
  Box
} from '@mui/material';

const ThemePreferenceSelector = () => {
  const [value, setValue] = useState('light');

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  return (
    <Paper elevation={3} sx={{ p: 3, maxWidth: 400, mx: 'auto' }}>
      <FormControl component="fieldset">
        <FormLabel component="legend">Theme Preference</FormLabel>
        <RadioGroup
          aria-label="theme-preference"
          name="theme-preference"
          value={value}
          onChange={handleChange}
        >
          <FormControlLabel value="light" control={<Radio />} label="Light" />
          <FormControlLabel value="dark" control={<Radio />} label="Dark" />
          <FormControlLabel value="system" control={<Radio />} label="System Default" />
        </RadioGroup>
      </FormControl>
      <Box mt={2}>
        Selected preference: <strong>{value}</strong>
      </Box>
    </Paper>
  );
};

export default ThemePreferenceSelector;

In this example, I've created a controlled component using React's useState hook. The value state variable stores the currently selected option, and the handleChange function updates this state when a user selects a different option.

The FormControl component provides context for the form element, while FormLabel gives it a descriptive label. The RadioGroup manages the radio buttons, ensuring only one can be selected at a time. Each option is represented by a FormControlLabel that wraps a Radio component with a text label.

Step 3: Integrate the Component in Your App

Now you can use the ThemePreferenceSelector in your application:

import React from 'react';
import { Container, Typography, Box } from '@mui/material';
import ThemePreferenceSelector from './ThemePreferenceSelector';

function App() {
  return (
    <Container maxWidth="md">
      <Box my={4}>
        <Typography variant="h4" component="h1" gutterBottom align="center">
          User Preferences
        </Typography>
        <ThemePreferenceSelector />
      </Box>
    </Container>
  );
}

export default App;

Enhancing the Preference Selector

Now that we have a basic implementation, let's enhance it with more features and customizations.

Organizing Radio Options with Data

Instead of hardcoding each radio option, we can make our component more flexible by using a data array:

import React, { useState } from 'react';
import {
  FormControl,
  FormLabel,
  RadioGroup,
  FormControlLabel,
  Radio,
  Paper,
  Box,
  Typography
} from '@mui/material';

const ThemePreferenceSelector = () => {
  const [value, setValue] = useState('light');

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const themeOptions = [
    { value: 'light', label: 'Light', description: 'Bright theme with light backgrounds' },
    { value: 'dark', label: 'Dark', description: 'Dark theme with light text for low-light environments' },
    { value: 'system', label: 'System Default', description: 'Follows your device settings' }
  ];

  return (
    <Paper elevation={3} sx={{ p: 3, maxWidth: 500, mx: 'auto' }}>
      <FormControl component="fieldset" fullWidth>
        <FormLabel component="legend">Theme Preference</FormLabel>
        <RadioGroup
          aria-label="theme-preference"
          name="theme-preference"
          value={value}
          onChange={handleChange}
        >
          {themeOptions.map((option) => (
            <FormControlLabel
              key={option.value}
              value={option.value}
              control={<Radio />}
              label={
                <Box>
                  <Typography variant="body1">{option.label}</Typography>
                  <Typography variant="body2" color="text.secondary">
                    {option.description}
                  </Typography>
                </Box>
              }
              sx={{ mb: 1 }}
            />
          ))}
        </RadioGroup>
      </FormControl>
      <Box mt={2} p={2} bgcolor="action.hover" borderRadius={1}>
        <Typography>
          Selected preference: <strong>{value}</strong>
        </Typography>
      </Box>
    </Paper>
  );
};

export default ThemePreferenceSelector;

This approach offers several advantages:

  • It's easier to add, remove, or modify options
  • You can include additional data for each option (like descriptions)
  • The component becomes more maintainable and scalable

Styling and Customization

MUI's Radio Group components can be styled in multiple ways. Let's explore some customization options:

Using the sx Prop

The sx prop is MUI's solution for one-off styling needs:

import React, { useState } from 'react';
import {
  FormControl,
  FormLabel,
  RadioGroup,
  FormControlLabel,
  Radio,
  Paper,
  Box,
  Typography
} from '@mui/material';

const ThemePreferenceSelector = () => {
  const [value, setValue] = useState('light');

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const themeOptions = [
    { value: 'light', label: 'Light', description: 'Bright theme with light backgrounds' },
    { value: 'dark', label: 'Dark', description: 'Dark theme with light text for low-light environments' },
    { value: 'system', label: 'System Default', description: 'Follows your device settings' }
  ];

  return (
    <Paper 
      elevation={3} 
      sx={{ 
        p: 3, 
        maxWidth: 500, 
        mx: 'auto',
        borderRadius: 2,
        bgcolor: 'background.paper'
      }}
    >
      <FormControl component="fieldset" fullWidth>
        <FormLabel 
          component="legend"
          sx={{ 
            fontSize: '1.1rem', 
            fontWeight: 'medium',
            mb: 2,
            color: 'primary.main'
          }}
        >
          Theme Preference
        </FormLabel>
        <RadioGroup
          aria-label="theme-preference"
          name="theme-preference"
          value={value}
          onChange={handleChange}
        >
          {themeOptions.map((option) => (
            <FormControlLabel
              key={option.value}
              value={option.value}
              control={
                <Radio 
                  sx={{ 
                    '&.Mui-checked': {
                      color: option.value === 'light' ? 'primary.main' : 
                             option.value === 'dark' ? 'secondary.main' : 
                             'success.main'
                    }
                  }}
                />
              }
              label={
                <Box sx={{ ml: 1 }}>
                  <Typography variant="body1" fontWeight={value === option.value ? 'bold' : 'regular'}>
                    {option.label}
                  </Typography>
                  <Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
                    {option.description}
                  </Typography>
                </Box>
              }
              sx={{ 
                mb: 2, 
                p: 1,
                borderRadius: 1,
                transition: 'background-color 0.2s',
                '&:hover': {
                  bgcolor: 'action.hover'
                },
                ...(value === option.value && {
                  bgcolor: 'action.selected'
                })
              }}
            />
          ))}
        </RadioGroup>
      </FormControl>
      <Box 
        mt={3} 
        p={2} 
        bgcolor="action.hover" 
        borderRadius={1}
        border="1px solid"
        borderColor="divider"
      >
        <Typography>
          Selected preference: <strong>{value}</strong>
        </Typography>
      </Box>
    </Paper>
  );
};

export default ThemePreferenceSelector;

This example demonstrates several styling techniques:

  • Custom colors for radio buttons based on the option value
  • Visual feedback for the selected option with background color
  • Hover effects for better interactivity
  • Custom typography for labels and descriptions
  • Spacing and layout adjustments for better readability

Theme Customization

For consistent styling across your application, you can customize the Radio components through the theme:

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

const theme = createTheme({
  components: {
    MuiRadio: {
      styleOverrides: {
        root: {
          '&.Mui-checked': {
            '& .MuiSvgIcon-root': {
              transform: 'scale(1.2)',
              transition: 'transform 0.2s'
            }
          }
        }
      }
    },
    MuiFormControlLabel: {
      styleOverrides: {
        root: {
          marginBottom: '12px',
          padding: '8px',
          borderRadius: '4px',
          transition: 'background-color 0.2s',
          '&:hover': {
            backgroundColor: 'rgba(0, 0, 0, 0.04)'
          }
        },
        label: {
          fontSize: '1rem'
        }
      }
    }
  }
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <div style={{ padding: '24px' }}>
        <ThemePreferenceSelector />
      </div>
    </ThemeProvider>
  );
}

export default App;

This approach allows you to define consistent styling for all Radio and FormControlLabel components throughout your application, making your UI more cohesive.

Building a Complete Preference Form

Now, let's build a more comprehensive preference form that includes multiple Radio Groups and demonstrates form submission.

import React, { useState } from 'react';
import {
  Box,
  Button,
  Divider,
  FormControl,
  FormControlLabel,
  FormHelperText,
  FormLabel,
  Paper,
  Radio,
  RadioGroup,
  Typography,
  Alert
} from '@mui/material';

const UserPreferencesForm = () => {
  const [preferences, setPreferences] = useState({
    theme: 'light',
    notifications: 'all',
    language: 'en',
    dataUsage: 'medium'
  });
  
  const [errors, setErrors] = useState({});
  const [submitted, setSubmitted] = useState(false);

  const handleChange = (event) => {
    const { name, value } = event.target;
    setPreferences({
      ...preferences,
      [name]: value
    });
    
    // Clear error when field is updated
    if (errors[name]) {
      setErrors({
        ...errors,
        [name]: null
      });
    }
    
    // Reset submitted status when form changes
    if (submitted) {
      setSubmitted(false);
    }
  };

  const validateForm = () => {
    const newErrors = {};
    
    // Example validation
    if (!preferences.theme) {
      newErrors.theme = 'Please select a theme preference';
    }
    if (!preferences.notifications) {
      newErrors.notifications = 'Please select a notification preference';
    }
    if (!preferences.language) {
      newErrors.language = 'Please select a language preference';
    }
    if (!preferences.dataUsage) {
      newErrors.dataUsage = 'Please select a data usage preference';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    
    if (validateForm()) {
      // In a real app, you would send this data to your backend
      console.log('Submitting preferences:', preferences);
      setSubmitted(true);
    }
  };

  return (
    <Paper elevation={3} sx={{ maxWidth: 600, mx: 'auto', p: 3 }}>
      <Typography variant="h5" component="h2" gutterBottom>
        User Preferences
      </Typography>
      
      {submitted && (
        <Alert severity="success" sx={{ mb: 3 }}>
          Your preferences have been saved successfully!
        </Alert>
      )}
      
      <form onSubmit={handleSubmit}>
        <Box mb={3}>
          <FormControl 
            component="fieldset" 
            fullWidth 
            error={!!errors.theme}
          >
            <FormLabel component="legend">Theme Preference</FormLabel>
            <RadioGroup
              name="theme"
              value={preferences.theme}
              onChange={handleChange}
            >
              <FormControlLabel value="light" control={<Radio />} label="Light" />
              <FormControlLabel value="dark" control={<Radio />} label="Dark" />
              <FormControlLabel value="system" control={<Radio />} label="System Default" />
            </RadioGroup>
            {errors.theme && <FormHelperText>{errors.theme}</FormHelperText>}
          </FormControl>
        </Box>
        
        <Divider sx={{ my: 2 }} />
        
        <Box mb={3}>
          <FormControl 
            component="fieldset" 
            fullWidth
            error={!!errors.notifications}
          >
            <FormLabel component="legend">Notification Preferences</FormLabel>
            <RadioGroup
              name="notifications"
              value={preferences.notifications}
              onChange={handleChange}
            >
              <FormControlLabel value="all" control={<Radio />} label="All Notifications" />
              <FormControlLabel value="important" control={<Radio />} label="Important Only" />
              <FormControlLabel value="none" control={<Radio />} label="No Notifications" />
            </RadioGroup>
            {errors.notifications && <FormHelperText>{errors.notifications}</FormHelperText>}
          </FormControl>
        </Box>
        
        <Divider sx={{ my: 2 }} />
        
        <Box mb={3}>
          <FormControl 
            component="fieldset" 
            fullWidth
            error={!!errors.language}
          >
            <FormLabel component="legend">Language Preference</FormLabel>
            <RadioGroup
              name="language"
              value={preferences.language}
              onChange={handleChange}
              row
            >
              <FormControlLabel value="en" control={<Radio />} label="English" />
              <FormControlLabel value="es" control={<Radio />} label="Spanish" />
              <FormControlLabel value="fr" control={<Radio />} label="French" />
              <FormControlLabel value="de" control={<Radio />} label="German" />
            </RadioGroup>
            {errors.language && <FormHelperText>{errors.language}</FormHelperText>}
          </FormControl>
        </Box>
        
        <Divider sx={{ my: 2 }} />
        
        <Box mb={3}>
          <FormControl 
            component="fieldset" 
            fullWidth
            error={!!errors.dataUsage}
          >
            <FormLabel component="legend">Data Usage</FormLabel>
            <RadioGroup
              name="dataUsage"
              value={preferences.dataUsage}
              onChange={handleChange}
            >
              <FormControlLabel 
                value="low" 
                control={<Radio />} 
                label={
                  <Box>
                    <Typography variant="body1">Low</Typography>
                    <Typography variant="body2" color="text.secondary">
                      Save data, lower quality images
                    </Typography>
                  </Box>
                }
              />
              <FormControlLabel 
                value="medium" 
                control={<Radio />} 
                label={
                  <Box>
                    <Typography variant="body1">Medium</Typography>
                    <Typography variant="body2" color="text.secondary">
                      Balanced quality and data usage
                    </Typography>
                  </Box>
                }
              />
              <FormControlLabel 
                value="high" 
                control={<Radio />} 
                label={
                  <Box>
                    <Typography variant="body1">High</Typography>
                    <Typography variant="body2" color="text.secondary">
                      Best quality, higher data usage
                    </Typography>
                  </Box>
                }
              />
            </RadioGroup>
            {errors.dataUsage && <FormHelperText>{errors.dataUsage}</FormHelperText>}
          </FormControl>
        </Box>
        
        <Box mt={4} display="flex" justifyContent="flex-end">
          <Button 
            type="button" 
            sx={{ mr: 2 }}
            onClick={() => {
              setPreferences({
                theme: 'light',
                notifications: 'all',
                language: 'en',
                dataUsage: 'medium'
              });
              setErrors({});
              setSubmitted(false);
            }}
          >
            Reset
          </Button>
          <Button 
            type="submit" 
            variant="contained" 
            color="primary"
          >
            Save Preferences
          </Button>
        </Box>
      </form>
    </Paper>
  );
};

export default UserPreferencesForm;

This comprehensive example demonstrates:

  • Managing multiple Radio Groups in a single form
  • Form validation with error messages
  • Handling form submission
  • Using FormHelperText for validation feedback
  • Different layouts for Radio Groups (vertical and horizontal)
  • Complex labels with descriptions
  • Form reset functionality
  • Success feedback after submission

Integration 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 MUI's Radio Group with React Hook Form:

import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import {
  Box,
  Button,
  FormControl,
  FormControlLabel,
  FormHelperText,
  FormLabel,
  Paper,
  Radio,
  RadioGroup,
  Typography,
  Alert
} from '@mui/material';

const PreferenceFormWithHookForm = () => {
  const { 
    control, 
    handleSubmit, 
    formState: { errors, isSubmitSuccessful },
    reset
  } = useForm({
    defaultValues: {
      theme: 'light',
      notifications: 'all'
    }
  });

  const onSubmit = (data) => {
    console.log('Form submitted:', data);
    // In a real app, you would send this data to your backend
  };

  return (
    <Paper elevation={3} sx={{ maxWidth: 600, mx: 'auto', p: 3 }}>
      <Typography variant="h5" component="h2" gutterBottom>
        User Preferences
      </Typography>
      
      {isSubmitSuccessful && (
        <Alert severity="success" sx={{ mb: 3 }}>
          Your preferences have been saved successfully!
        </Alert>
      )}
      
      <form onSubmit={handleSubmit(onSubmit)}>
        <Box mb={3}>
          <Controller
            name="theme"
            control={control}
            rules={{ required: 'Please select a theme preference' }}
            render={({ field }) => (
              <FormControl 
                component="fieldset" 
                fullWidth 
                error={!!errors.theme}
              >
                <FormLabel component="legend">Theme Preference</FormLabel>
                <RadioGroup {...field}>
                  <FormControlLabel value="light" control={<Radio />} label="Light" />
                  <FormControlLabel value="dark" control={<Radio />} label="Dark" />
                  <FormControlLabel value="system" control={<Radio />} label="System Default" />
                </RadioGroup>
                {errors.theme && (
                  <FormHelperText>{errors.theme.message}</FormHelperText>
                )}
              </FormControl>
            )}
          />
        </Box>
        
        <Box mb={3}>
          <Controller
            name="notifications"
            control={control}
            rules={{ required: 'Please select a notification preference' }}
            render={({ field }) => (
              <FormControl 
                component="fieldset" 
                fullWidth
                error={!!errors.notifications}
              >
                <FormLabel component="legend">Notification Preferences</FormLabel>
                <RadioGroup {...field}>
                  <FormControlLabel value="all" control={<Radio />} label="All Notifications" />
                  <FormControlLabel value="important" control={<Radio />} label="Important Only" />
                  <FormControlLabel value="none" control={<Radio />} label="No Notifications" />
                </RadioGroup>
                {errors.notifications && (
                  <FormHelperText>{errors.notifications.message}</FormHelperText>
                )}
              </FormControl>
            )}
          />
        </Box>
        
        <Box mt={4} display="flex" justifyContent="flex-end">
          <Button 
            type="button" 
            sx={{ mr: 2 }}
            onClick={() => reset()}
          >
            Reset
          </Button>
          <Button 
            type="submit" 
            variant="contained" 
            color="primary"
          >
            Save Preferences
          </Button>
        </Box>
      </form>
    </Paper>
  );
};

export default PreferenceFormWithHookForm;

Using React Hook Form provides several advantages:

  • Simplified form validation
  • Better performance through reduced re-renders
  • Built-in form state management
  • Easy access to form status (dirty, touched, etc.)
  • Simplified error handling

Advanced Customization Techniques

Let's explore some advanced customization techniques for the Radio Group component.

Custom Radio Buttons

You can completely customize the appearance of radio buttons while maintaining their functionality:

import React, { useState } from 'react';
import {
  Box,
  FormControl,
  FormControlLabel,
  FormLabel,
  Paper,
  Radio,
  RadioGroup,
  Typography,
  useTheme
} from '@mui/material';
import LightModeIcon from '@mui/icons-material/LightMode';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import SettingsBrightnessIcon from '@mui/icons-material/SettingsBrightness';

const CustomRadioPreferenceSelector = () => {
  const [value, setValue] = useState('light');
  const theme = useTheme();

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const themeOptions = [
    { 
      value: 'light', 
      label: 'Light Theme', 
      icon: <LightModeIcon />,
      color: theme.palette.primary.main
    },
    { 
      value: 'dark', 
      label: 'Dark Theme', 
      icon: <DarkModeIcon />,
      color: theme.palette.secondary.main
    },
    { 
      value: 'system', 
      label: 'System Default', 
      icon: <SettingsBrightnessIcon />,
      color: theme.palette.success.main
    }
  ];

  return (
    <Paper elevation={3} sx={{ p: 3, maxWidth: 500, mx: 'auto' }}>
      <FormControl component="fieldset" fullWidth>
        <FormLabel component="legend" sx={{ mb: 2 }}>
          Theme Preference
        </FormLabel>
        <RadioGroup
          aria-label="theme-preference"
          name="theme-preference"
          value={value}
          onChange={handleChange}
        >
          <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
            {themeOptions.map((option) => (
              <Paper
                key={option.value}
                elevation={value === option.value ? 3 : 1}
                sx={{
                  p: 2,
                  borderRadius: 2,
                  cursor: 'pointer',
                  transition: 'all 0.2s',
                  border: '2px solid',
                  borderColor: value === option.value ? option.color : 'transparent',
                  '&:hover': {
                    backgroundColor: theme.palette.action.hover
                  }
                }}
                onClick={() => setValue(option.value)}
              >
                <FormControlLabel
                  value={option.value}
                  control={
                    <Radio
                      sx={{
                        '&.Mui-checked': {
                          color: option.color
                        }
                      }}
                    />
                  }
                  label={
                    <Box sx={{ display: 'flex', alignItems: 'center' }}>
                      <Box 
                        sx={{ 
                          mr: 1.5,
                          color: value === option.value ? option.color : 'text.secondary',
                          display: 'flex'
                        }}
                      >
                        {option.icon}
                      </Box>
                      <Typography 
                        variant="body1"
                        fontWeight={value === option.value ? 'bold' : 'regular'}
                      >
                        {option.label}
                      </Typography>
                    </Box>
                  }
                  sx={{
                    margin: 0,
                    width: '100%'
                  }}
                />
              </Paper>
            ))}
          </Box>
        </RadioGroup>
      </FormControl>
    </Paper>
  );
};

export default CustomRadioPreferenceSelector;

This example creates a highly customized radio selector with:

  • Custom icons for each option
  • Card-like appearance for each option
  • Visual feedback (elevation and border) for the selected option
  • Color coding for different options

Creating a Radio Group with Images

For more visually oriented preferences, you can create a radio group with images:

import React, { useState } from 'react';
import {
  Box,
  FormControl,
  FormControlLabel,
  FormLabel,
  Grid,
  Paper,
  Radio,
  RadioGroup,
  Typography
} from '@mui/material';

const LayoutPreferenceSelector = () => {
  const [value, setValue] = useState('compact');

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const layoutOptions = [
    {
      value: 'compact',
      label: 'Compact',
      description: 'Maximum content, minimal spacing',
      image: 'https://via.placeholder.com/150x100?text=Compact'
    },
    {
      value: 'comfortable',
      label: 'Comfortable',
      description: 'Balanced layout with moderate spacing',
      image: 'https://via.placeholder.com/150x100?text=Comfortable'
    },
    {
      value: 'spacious',
      label: 'Spacious',
      description: 'Maximum readability with ample white space',
      image: 'https://via.placeholder.com/150x100?text=Spacious'
    }
  ];

  return (
    <Paper elevation={3} sx={{ p: 3, maxWidth: 800, mx: 'auto' }}>
      <FormControl component="fieldset" fullWidth>
        <FormLabel component="legend" sx={{ mb: 2 }}>
          Layout Preference
        </FormLabel>
        <RadioGroup
          aria-label="layout-preference"
          name="layout-preference"
          value={value}
          onChange={handleChange}
        >
          <Grid container spacing={2}>
            {layoutOptions.map((option) => (
              <Grid item xs={12} sm={4} key={option.value}>
                <Paper
                  elevation={value === option.value ? 4 : 1}
                  sx={{
                    p: 2,
                    height: '100%',
                    display: 'flex',
                    flexDirection: 'column',
                    cursor: 'pointer',
                    transition: 'all 0.2s',
                    border: '2px solid',
                    borderColor: value === option.value ? 'primary.main' : 'transparent',
                    '&:hover': {
                      boxShadow: 3
                    }
                  }}
                  onClick={() => setValue(option.value)}
                >
                  <Box 
                    component="img"
                    src={option.image}
                    alt={option.label}
                    sx={{
                      width: '100%',
                      height: 'auto',
                      borderRadius: 1,
                      mb: 2
                    }}
                  />
                  <FormControlLabel
                    value={option.value}
                    control={<Radio />}
                    label={
                      <Box>
                        <Typography variant="body1" fontWeight="medium">
                          {option.label}
                        </Typography>
                        <Typography variant="body2" color="text.secondary">
                          {option.description}
                        </Typography>
                      </Box>
                    }
                    sx={{
                      margin: 0,
                      alignItems: 'flex-start'
                    }}
                  />
                </Paper>
              </Grid>
            ))}
          </Grid>
        </RadioGroup>
      </FormControl>
    </Paper>
  );
};

export default LayoutPreferenceSelector;

This example creates a grid-based layout preference selector with:

  • Visual representations of each layout option
  • Responsive grid layout
  • Card-based selection interface
  • Detailed descriptions for each option

Accessibility Considerations

Accessibility is crucial for all form elements, including Radio Groups. Here are some best practices to ensure your Radio Group components are accessible:

Proper Labeling

Always use FormLabel to provide a clear label for the RadioGroup. This helps screen reader users understand the purpose of the form control.

ARIA Attributes

The RadioGroup component automatically adds the appropriate ARIA attributes, but you should always include the aria-label attribute to provide additional context:

<RadioGroup
  aria-label="theme-preference"
  name="theme-preference"
  value={value}
  onChange={handleChange}
>
  {/* Radio options */}
</RadioGroup>

Keyboard Navigation

MUI's Radio components are designed to be keyboard accessible. Users can:

  • Navigate between radio buttons using Tab and Shift+Tab
  • Select an option using Space
  • Navigate within a RadioGroup using arrow keys

Focus Visibility

Ensure that focus states are clearly visible for keyboard users. MUI provides this by default, but you should be careful not to override these styles in your customizations.

Enhanced Accessibility Example

Here's an example that implements additional accessibility features:

import React, { useState } from 'react';
import {
  Box,
  FormControl,
  FormControlLabel,
  FormHelperText,
  FormLabel,
  Paper,
  Radio,
  RadioGroup,
  Typography
} from '@mui/material';

const AccessiblePreferenceSelector = () => {
  const [value, setValue] = useState('light');

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const themeOptions = [
    { value: 'light', label: 'Light Theme', description: 'Bright theme with light backgrounds' },
    { value: 'dark', label: 'Dark Theme', description: 'Dark theme with light text for low-light environments' },
    { value: 'system', label: 'System Default', description: 'Follows your device settings' }
  ];

  // Generate a unique ID for this form control
  const formId = "theme-preference-selector";
  const labelId = `${formId}-label`;
  const descriptionId = `${formId}-description`;

  return (
    <Paper elevation={3} sx={{ p: 3, maxWidth: 500, mx: 'auto' }}>
      <FormControl component="fieldset" fullWidth>
        <FormLabel 
          id={labelId}
          component="legend"
        >
          Theme Preference
        </FormLabel>
        
        <FormHelperText id={descriptionId}>
          Select your preferred theme for the application interface
        </FormHelperText>
        
        <RadioGroup
          aria-labelledby={labelId}
          aria-describedby={descriptionId}
          name="theme-preference"
          value={value}
          onChange={handleChange}
        >
          {themeOptions.map((option) => {
            const optionId = `${formId}-${option.value}`;
            const optionDescriptionId = `${optionId}-description`;
            
            return (
              <Box key={option.value} mb={2}>
                <FormControlLabel
                  value={option.value}
                  control={<Radio id={optionId} />}
                  label={
                    <Box>
                      <Typography variant="body1">{option.label}</Typography>
                    </Box>
                  }
                />
                <Typography 
                  id={optionDescriptionId}
                  variant="body2" 
                  color="text.secondary"
                  sx={{ ml: 4 }}
                >
                  {option.description}
                </Typography>
              </Box>
            );
          })}
        </RadioGroup>
      </FormControl>
    </Paper>
  );
};

export default AccessiblePreferenceSelector;

This example implements several accessibility best practices:

  • Unique IDs for form elements
  • Proper ARIA attributes connecting labels, descriptions, and controls
  • Semantic HTML structure
  • Additional descriptive text for each option
  • Adequate spacing and visual hierarchy

Common Issues and Solutions

When working with MUI Radio Groups, you might encounter some common issues. Here are solutions to these problems:

Issue 1: Radio Buttons Not Updating When Clicked

This typically happens when you're not properly handling the state change:

// Incorrect
const RadioGroupExample = () => {
  const [value, setValue] = useState('option1');
  
  return (
    <RadioGroup value={value}>
      <FormControlLabel value="option1" control={<Radio />} label="Option 1" />
      <FormControlLabel value="option2" control={<Radio />} label="Option 2" />
    </RadioGroup>
  );
};

// Correct
const RadioGroupExample = () => {
  const [value, setValue] = useState('option1');
  
  const handleChange = (event) => {
    setValue(event.target.value);
  };
  
  return (
    <RadioGroup value={value} onChange={handleChange}>
      <FormControlLabel value="option1" control={<Radio />} label="Option 1" />
      <FormControlLabel value="option2" control={<Radio />} label="Option 2" />
    </RadioGroup>
  );
};

Issue 2: Form Submission Not Including Radio Values

This can happen if you're not using the name attribute correctly:

// Incorrect
<RadioGroup value={value} onChange={handleChange}>
  <FormControlLabel value="option1" control={<Radio />} label="Option 1" />
  <FormControlLabel value="option2" control={<Radio />} label="Option 2" />
</RadioGroup>

// Correct
<RadioGroup name="options" value={value} onChange={handleChange}>
  <FormControlLabel value="option1" control={<Radio />} label="Option 1" />
  <FormControlLabel value="option2" control={<Radio />} label="Option 2" />
</RadioGroup>

Issue 3: Initial Value Not Selected

If your initial value isn't showing as selected, make sure it matches one of the option values exactly:

// Incorrect (value types don't match)
const [value, setValue] = useState(1);

// In the RadioGroup:
<FormControlLabel value="1" control={<Radio />} label="Option 1" />

// Correct (consistent value types)
const [value, setValue] = useState('1');

// In the RadioGroup:
<FormControlLabel value="1" control={<Radio />} label="Option 1" />

Issue 4: Radio Buttons Not Aligning Properly

If your radio buttons aren't aligning properly with their labels, you can adjust the alignment:

// Solution
<FormControlLabel
  value="option1"
  control={<Radio />}
  label="Option 1"
  sx={{
    alignItems: 'flex-start', // Aligns radio with the top of the label
    '.MuiFormControlLabel-label': {
      mt: 0.5 // Fine-tune vertical alignment
    }
  }}
/>

Issue 5: Performance Issues with Large Radio Groups

For very large radio groups, you might experience performance issues. Consider using virtualization:

import React, { useState } from 'react';
import { FixedSizeList } from 'react-window';
import {
  FormControl,
  FormControlLabel,
  FormLabel,
  Paper,
  Radio,
  RadioGroup
} from '@mui/material';

const VirtualizedRadioGroup = () => {
  const [value, setValue] = useState('option1');
  
  const handleChange = (event) => {
    setValue(event.target.value);
  };
  
  // Generate a large number of options
  const options = Array.from({ length: 1000 }, (_, i) => ({
    value: `option${i + 1}`,
    label: `Option ${i + 1}`
  }));
  
  const Row = ({ index, style }) => {
    const option = options[index];
    
    return (
      <div style={style}>
        <FormControlLabel
          value={option.value}
          control={<Radio />}
          label={option.label}
          checked={value === option.value}
          onChange={handleChange}
        />
      </div>
    );
  };
  
  return (
    <Paper elevation={3} sx={{ p: 3, maxWidth: 500, mx: 'auto' }}>
      <FormControl component="fieldset" fullWidth>
        <FormLabel component="legend">Select an Option</FormLabel>
        <RadioGroup
          aria-label="options"
          name="options"
          value={value}
          onChange={handleChange}
        >
          <FixedSizeList
            height={300}
            width="100%"
            itemSize={48}
            itemCount={options.length}
            overscanCount={5}
          >
            {Row}
          </FixedSizeList>
        </RadioGroup>
      </FormControl>
    </Paper>
  );
};

export default VirtualizedRadioGroup;

This example uses react-window to virtualize a large list of radio options, significantly improving performance by only rendering the visible items.

Best Practices for MUI Radio Groups

To get the most out of MUI's Radio Group component, follow these best practices:

1. Always Use Controlled Components for Complex Forms

Controlled components give you more predictable behavior and better integration with React's state management:

const [value, setValue] = useState('default');

const handleChange = (event) => {
  setValue(event.target.value);
};

return (
  <RadioGroup
    value={value}
    onChange={handleChange}
    name="radio-group"
  >
    {/* Radio options */}
  </RadioGroup>
);

Use FormControl and FormLabel to create a semantic grouping of related radio options:

<FormControl component="fieldset">
  <FormLabel component="legend">Shipping Method</FormLabel>
  <RadioGroup value={value} onChange={handleChange}>
    {/* Radio options */}
  </RadioGroup>
</FormControl>

3. Provide Descriptive Labels

Use clear, concise labels that accurately describe each option:

<FormControlLabel
  value="standard"
  control={<Radio />}
  label={
    <Box>
      <Typography variant="body1">Standard Shipping</Typography>
      <Typography variant="body2" color="text.secondary">
        3-5 business days, $5.99
      </Typography>
    </Box>
  }
/>

4. Use Consistent Value Types

Ensure that your state value and option values use consistent types to avoid unexpected behavior:

// Consistent string values
const [value, setValue] = useState('option1');

// In the RadioGroup:
<FormControlLabel value="option1" control={<Radio />} label="Option 1" />
<FormControlLabel value="option2" control={<Radio />} label="Option 2" />

5. Implement Form Validation

Always validate user selections, especially for required fields:

const [value, setValue] = useState('');
const [error, setError] = useState(false);

const handleChange = (event) => {
  setValue(event.target.value);
  setError(false);
};

const handleSubmit = (event) => {
  event.preventDefault();
  if (!value) {
    setError(true);
    return;
  }
  // Process form submission
};

return (
  <FormControl error={error} component="fieldset">
    <FormLabel component="legend">Required Selection</FormLabel>
    <RadioGroup value={value} onChange={handleChange}>
      {/* Radio options */}
    </RadioGroup>
    {error && <FormHelperText>Please select an option</FormHelperText>}
  </FormControl>
);

Wrapping Up

The MUI Radio Group component is a versatile tool for building preference selectors in React applications. We've explored everything from basic implementation to advanced customization, form integration, and accessibility considerations. By following the best practices and examples in this guide, you can create intuitive, accessible, and visually appealing preference selectors that enhance the user experience of your application.

Remember that a well-designed preference selector should be intuitive, accessible, and visually consistent with your application's design language. The MUI Radio Group component provides all the building blocks you need to achieve these goals while maintaining a high level of customization and flexibility.