Menu

Mastering MUI ButtonGroup: Building Cohesive Action Controls in React Applications

As a front-end developer working with React, you've likely encountered scenarios where multiple related actions need to be presented together in a clean, organized interface. Whether it's a document editor's formatting toolbar, a data filtering panel, or a media player's control set, grouping related buttons is a common UI pattern that improves usability and visual cohesion.

Material UI's ButtonGroup component offers an elegant solution for this exact need. In this comprehensive guide, I'll walk you through everything you need to know about implementing ButtonGroup in your React applications—from basic usage to advanced customization techniques that I've refined over years of production development.

What You'll Learn

By the end of this article, you'll be able to:

  • Implement basic ButtonGroup components with various orientations and variants
  • Customize ButtonGroup appearance through theming and direct styling
  • Create split buttons and dropdown combinations
  • Build responsive toolbars with grouped actions
  • Handle complex interaction patterns and state management
  • Solve common implementation challenges and accessibility concerns

Let's dive into the details of this powerful yet often underutilized MUI component.

Understanding the ButtonGroup Component

The ButtonGroup component in Material UI serves a specific purpose: to group related buttons together in a visually cohesive unit. This grouping creates a stronger visual relationship between actions that are conceptually related, improving both the aesthetics and usability of your interface.

At its core, ButtonGroup is a wrapper that modifies how individual Button components render when placed adjacent to each other. It handles the styling of borders, spacing, and visual continuity between buttons, ensuring they appear as a unified control rather than separate elements.

Before we dive into implementation details, it's important to understand that ButtonGroup inherits many properties from the Button component. This means that props like variant, color, size, and disabled can be applied at the group level and will cascade to all child buttons (unless overridden at the individual button level).

Key Features and Props

ButtonGroup comes with several key props that control its appearance and behavior. Let's examine the most important ones:

PropTypeDefaultDescription
childrennode-The content of the component, typically Button elements
color

'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' | string

'primary'The color of the component
disabledboolfalseIf true, all buttons will be disabled
disableElevationboolfalseIf true, no elevation is applied to the buttons
disableRippleboolfalseIf true, the ripple effect is disabled for all buttons
fullWidthboolfalse

If true, the buttons will take up the full width of their container

orientation'horizontal' | 'vertical''horizontal'The orientation of the button group
size'small' | 'medium' | 'large''medium'The size of the component
variant'contained' | 'outlined' | 'text''outlined'The variant to use
sxobject-The system prop that allows defining system overrides

One key distinction to note is that unlike many other MUI components, ButtonGroup's default variant is 'outlined' rather than 'contained'. This is intentional, as outlined buttons often work better visually when grouped together.

Variants and Visual Options

ButtonGroup supports the same three variants as the standard Button component:

  1. Outlined (default): Buttons with a border outline and transparent background
  2. Contained: Solid buttons with background color and elevation
  3. Text: Buttons without a background or border, showing only text

The choice of variant affects not just the appearance of individual buttons but also how they're visually connected in the group. For example, contained buttons in a group will share borders and appear as a single unit, while text buttons will appear more subtly connected.

Getting Started with ButtonGroup

Let's start with the basics: implementing a simple horizontal ButtonGroup with three actions. First, ensure you have the necessary dependencies installed:

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

Now, let's create a basic ButtonGroup component:

import React from 'react';
import { ButtonGroup, Button } from '@mui/material';

function BasicButtonGroup() {
return (
<ButtonGroup variant="contained" aria-label="formatting options">
<Button>Bold</Button>
<Button>Italic</Button>
<Button>Underline</Button>
</ButtonGroup>
);
}

export default BasicButtonGroup;

This creates a simple horizontal group of three contained buttons. The aria-label provides accessibility context for screen readers.

Let's explore how different variants affect the appearance:

import React from 'react';
import { Stack, ButtonGroup, Button } from '@mui/material';

function ButtonGroupVariants() {
return (
<Stack spacing={2} direction="column" alignItems="flex-start">
<ButtonGroup variant="contained" aria-label="contained button group">
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>

      <ButtonGroup variant="outlined" aria-label="outlined button group">
        <Button>One</Button>
        <Button>Two</Button>
        <Button>Three</Button>
      </ButtonGroup>

      <ButtonGroup variant="text" aria-label="text button group">
        <Button>One</Button>
        <Button>Two</Button>
        <Button>Three</Button>
      </ButtonGroup>
    </Stack>

);
}

export default ButtonGroupVariants;

In this example, we're using MUI's Stack component to display three different ButtonGroup variants vertically. Each group demonstrates a different visual style while maintaining the grouped relationship between buttons.

Orientation Options

ButtonGroup can be oriented either horizontally (the default) or vertically, depending on your layout needs:

import React from 'react';
import { Stack, ButtonGroup, Button } from '@mui/material';

function ButtonGroupOrientation() {
return (
<Stack spacing={2} direction="row" alignItems="center">
<ButtonGroup
        orientation="vertical"
        variant="contained"
        aria-label="vertical contained button group"
      >
<Button>Top</Button>
<Button>Middle</Button>
<Button>Bottom</Button>
</ButtonGroup>

      <ButtonGroup
        orientation="vertical"
        variant="outlined"
        aria-label="vertical outlined button group"
      >
        <Button>Top</Button>
        <Button>Middle</Button>
        <Button>Bottom</Button>
      </ButtonGroup>
    </Stack>

);
}

export default ButtonGroupOrientation;

Vertical orientation is particularly useful for sidebar toolbars or when you need to conserve horizontal space. The visual connection between buttons is maintained, but the flow direction changes.

Creating a Practical Toolbar with ButtonGroup

Let's build something more practical: a text formatting toolbar that might be used in a rich text editor. We'll use icons from @mui/icons-material to create a more visually intuitive interface:

import React, { useState } from 'react';
import { 
  ButtonGroup, 
  Button, 
  Divider, 
  Paper,
  Stack,
  Typography
} from '@mui/material';
import { 
  FormatBold, 
  FormatItalic, 
  FormatUnderlined,
  FormatAlignLeft,
  FormatAlignCenter,
  FormatAlignRight,
  FormatAlignJustify
} from '@mui/icons-material';

function TextFormattingToolbar() {
const [formats, setFormats] = useState({
bold: false,
italic: false,
underline: false,
alignment: 'left'
});

const handleFormatToggle = (format) => {
setFormats(prev => ({
...prev,
[format]: !prev[format]
}));
};

const handleAlignmentChange = (alignment) => {
setFormats(prev => ({
...prev,
alignment
}));
};

return (
<Stack spacing={2}>
<Paper elevation={0} sx={{ p: 2, border: '1px solid #e0e0e0' }}>
<Stack direction="row" spacing={1} alignItems="center">
{/* Text formatting controls */}
<ButtonGroup variant="outlined" size="small" aria-label="text formatting">
<Button
color={formats.bold ? "primary" : "inherit"}
onClick={() => handleFormatToggle('bold')}
aria-label="bold" >
<FormatBold />
</Button>
<Button
color={formats.italic ? "primary" : "inherit"}
onClick={() => handleFormatToggle('italic')}
aria-label="italic" >
<FormatItalic />
</Button>
<Button
color={formats.underline ? "primary" : "inherit"}
onClick={() => handleFormatToggle('underline')}
aria-label="underline" >
<FormatUnderlined />
</Button>
</ButtonGroup>

          <Divider orientation="vertical" flexItem />

          {/* Alignment controls */}
          <ButtonGroup variant="outlined" size="small" aria-label="text alignment">
            <Button
              color={formats.alignment === 'left' ? "primary" : "inherit"}
              onClick={() => handleAlignmentChange('left')}
              aria-label="align left"
            >
              <FormatAlignLeft />
            </Button>
            <Button
              color={formats.alignment === 'center' ? "primary" : "inherit"}
              onClick={() => handleAlignmentChange('center')}
              aria-label="align center"
            >
              <FormatAlignCenter />
            </Button>
            <Button
              color={formats.alignment === 'right' ? "primary" : "inherit"}
              onClick={() => handleAlignmentChange('right')}
              aria-label="align right"
            >
              <FormatAlignRight />
            </Button>
            <Button
              color={formats.alignment === 'justify' ? "primary" : "inherit"}
              onClick={() => handleAlignmentChange('justify')}
              aria-label="align justify"
            >
              <FormatAlignJustify />
            </Button>
          </ButtonGroup>
        </Stack>
      </Paper>

      {/* Preview text area with applied formatting */}
      <Paper
        elevation={0}
        sx={{
          p: 2,
          border: '1px solid #e0e0e0',
          minHeight: 100,
          textAlign: formats.alignment,
          fontWeight: formats.bold ? 'bold' : 'normal',
          fontStyle: formats.italic ? 'italic' : 'normal',
          textDecoration: formats.underline ? 'underline' : 'none'
        }}
      >
        <Typography>
          This is a sample text that demonstrates the formatting options applied from the toolbar above.
          Try clicking the different formatting buttons to see how they affect this text.
        </Typography>
      </Paper>
    </Stack>

);
}

export default TextFormattingToolbar;

This example demonstrates several important concepts:

  1. Using ButtonGroup to organize related controls (text formatting in one group, alignment in another)
  2. Managing state to track which formatting options are active
  3. Visually indicating active state by changing button colors
  4. Using icons for better visual recognition
  5. Separating button groups with dividers for logical grouping
  6. Providing a live preview of the formatting options

The resulting toolbar is both functional and visually cohesive, with clear grouping of related actions.

Customizing ButtonGroup Appearance

While the default appearance of ButtonGroup works well in many cases, you'll often want to customize it to match your application's design system. MUI provides several approaches for customization.

Using the sx Prop for Direct Styling

The sx prop is the most direct way to apply custom styles to a ButtonGroup:

import React from 'react';
import { ButtonGroup, Button } from '@mui/material';

function CustomStyledButtonGroup() {
return (
<ButtonGroup
variant="contained"
aria-label="custom styled button group"
sx={{
        '& .MuiButton-root': {
          borderRadius: '4px',
          margin: '0 2px',
          borderRight: '1px solid rgba(255, 255, 255, 0.3) !important',
          '&:first-of-type': {
            borderTopLeftRadius: '20px',
            borderBottomLeftRadius: '20px',
          },
          '&:last-of-type': {
            borderTopRightRadius: '20px',
            borderBottomRightRadius: '20px',
            borderRight: 'none !important',
          },
        },
        boxShadow: '0 4px 10px rgba(0, 0, 0, 0.15)',
        background: 'linear-gradient(45deg, #2196F3 30%, #21CBF3 90%)',
      }} >
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
);
}

export default CustomStyledButtonGroup;

This example applies several custom styles:

  • A gradient background to the entire button group
  • Custom box shadow
  • Rounded edges on the first and last buttons
  • Custom spacing between buttons
  • Custom border styling

Theme Customization for Consistent Styling

For application-wide styling, it's better to customize the theme:

import React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { ButtonGroup, Button, Stack, Typography } from '@mui/material';

function ThemedButtonGroups() {
// Create a custom theme with ButtonGroup overrides
const theme = createTheme({
components: {
MuiButtonGroup: {
styleOverrides: {
root: {
boxShadow: 'none',
},
grouped: {
'&:not(:last-of-type)': {
borderRight: '1px solid rgba(0, 0, 0, 0.12)',
},
},
},
variants: [
{
props: { variant: 'contained', color: 'primary' },
style: {
'& .MuiButton-root': {
background: 'linear-gradient(45deg, #2196F3 30%, #21CBF3 90%)',
color: 'white',
borderRight: '1px solid rgba(255, 255, 255, 0.3) !important',
'&:last-of-type': {
borderRight: 'none !important',
},
},
},
},
{
props: { variant: 'outlined', color: 'secondary' },
style: {
borderRadius: '24px',
'& .MuiButton-root': {
borderColor: '#ff4081',
color: '#ff4081',
'&:hover': {
backgroundColor: 'rgba(255, 64, 129, 0.08)',
},
},
},
},
],
},
},
});

return (
<ThemeProvider theme={theme}>
<Stack spacing={3} alignItems="flex-start">
<div>
<Typography variant="subtitle1" gutterBottom>
Themed Primary Contained ButtonGroup
</Typography>
<ButtonGroup variant="contained" color="primary">
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
</div>

        <div>
          <Typography variant="subtitle1" gutterBottom>
            Themed Secondary Outlined ButtonGroup
          </Typography>
          <ButtonGroup variant="outlined" color="secondary">
            <Button>One</Button>
            <Button>Two</Button>
            <Button>Three</Button>
          </ButtonGroup>
        </div>
      </Stack>
    </ThemeProvider>

);
}

export default ThemedButtonGroups;

This approach uses MUI's theming system to define global styles for ButtonGroup components. The advantages include:

  1. Consistent styling across the application
  2. Separation of styling from component logic
  3. The ability to define variants that can be reused
  4. Easier maintenance when design changes are needed

Advanced ButtonGroup Patterns

Let's explore some more advanced patterns that solve common UI challenges.

Split Buttons with Dropdown Menus

A split button combines a primary action with a dropdown menu of related actions:

import React, { useState } from 'react';
import { 
  ButtonGroup, 
  Button, 
  ClickAwayListener,
  Grow,
  Paper,
  Popper,
  MenuItem,
  MenuList
} from '@mui/material';
import { ArrowDropDown } from '@mui/icons-material';

function SplitButton() {
const options = ['Create a merge commit', 'Squash and merge', 'Rebase and merge'];
const [open, setOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const anchorRef = React.useRef(null);

const handleClick = () => {
console.info(`You clicked ${options[selectedIndex]}`);
};

const handleMenuItemClick = (event, index) => {
setSelectedIndex(index);
setOpen(false);
};

const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};

const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};

return (
<>
<ButtonGroup variant="contained" ref={anchorRef} aria-label="split button">
<Button onClick={handleClick}>{options[selectedIndex]}</Button>
<Button
size="small"
aria-controls={open ? 'split-button-menu' : undefined}
aria-expanded={open ? 'true' : undefined}
aria-label="select merge strategy"
aria-haspopup="menu"
onClick={handleToggle} >
<ArrowDropDown />
</Button>
</ButtonGroup>
<Popper
sx={{
          zIndex: 1,
        }}
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal >
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
              transformOrigin:
                placement === 'bottom' ? 'center top' : 'center bottom',
            }} >
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu" autoFocusItem>
{options.map((option, index) => (
<MenuItem
key={option}
selected={index === selectedIndex}
onClick={(event) => handleMenuItemClick(event, index)} >
{option}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</>
);
}

export default SplitButton;

This pattern is useful when you have a primary action but want to offer variations of that action. The main button performs the currently selected action, while the dropdown button allows the user to select a different action variant.

Responsive ButtonGroup with Icon/Text Toggle

For responsive designs, you might want to show only icons on small screens and icons with text on larger screens:

import React from 'react';
import { 
  ButtonGroup, 
  Button, 
  useMediaQuery, 
  useTheme 
} from '@mui/material';
import { 
  Save as SaveIcon, 
  Delete as DeleteIcon, 
  Edit as EditIcon, 
  FileCopy as CopyIcon 
} from '@mui/icons-material';

function ResponsiveButtonGroup() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));

return (
<ButtonGroup variant="contained" aria-label="document actions">
{isMobile ? (
// Mobile view - Icons only
<>
<Button aria-label="save document">
<SaveIcon />
</Button>
<Button aria-label="edit document">
<EditIcon />
</Button>
<Button aria-label="copy document">
<CopyIcon />
</Button>
<Button aria-label="delete document" color="error">
<DeleteIcon />
</Button>
</>
) : (
// Desktop view - Icons with text
<>
<Button startIcon={<SaveIcon />}>Save</Button>
<Button startIcon={<EditIcon />}>Edit</Button>
<Button startIcon={<CopyIcon />}>Copy</Button>
<Button startIcon={<DeleteIcon />} color="error">Delete</Button>
</>
)}
</ButtonGroup>
);
}

export default ResponsiveButtonGroup;

This component uses MUI's useMediaQuery hook to detect screen size and render different button content accordingly. On mobile devices, only icons are shown to conserve space, while larger screens get the full text labels alongside icons.

Segmented Controls (Toggle Button Group)

For mutually exclusive options, MUI provides a related component called ToggleButtonGroup that works similarly to ButtonGroup but with built-in selection state:

import React, { useState } from 'react';
import { 
  ToggleButtonGroup, 
  ToggleButton 
} from '@mui/material';
import {
  FormatAlignLeft,
  FormatAlignCenter,
  FormatAlignRight,
  FormatAlignJustify
} from '@mui/icons-material';

function TextAlignment() {
const [alignment, setAlignment] = useState('left');

const handleAlignment = (event, newAlignment) => {
if (newAlignment !== null) {
setAlignment(newAlignment);
}
};

return (
<ToggleButtonGroup
      value={alignment}
      exclusive
      onChange={handleAlignment}
      aria-label="text alignment"
    >
<ToggleButton value="left" aria-label="left aligned">
<FormatAlignLeft />
</ToggleButton>
<ToggleButton value="center" aria-label="centered">
<FormatAlignCenter />
</ToggleButton>
<ToggleButton value="right" aria-label="right aligned">
<FormatAlignRight />
</ToggleButton>
<ToggleButton value="justify" aria-label="justified">
<FormatAlignJustify />
</ToggleButton>
</ToggleButtonGroup>
);
}

export default TextAlignment;

While not technically a ButtonGroup, ToggleButtonGroup provides similar visual grouping with built-in state management for selection. The exclusive prop ensures that only one button can be selected at a time, making it perfect for mutually exclusive options.

Building a Complete Toolbar with ButtonGroup

Now let's combine several concepts to build a more complete document editing toolbar:

import React, { useState } from 'react';
import {
  AppBar,
  Toolbar,
  ButtonGroup,
  Button,
  Divider,
  ToggleButtonGroup,
  ToggleButton,
  Menu,
  MenuItem,
  ListItemIcon,
  ListItemText,
  useMediaQuery,
  useTheme,
  IconButton
} from '@mui/material';
import {
  FormatBold,
  FormatItalic,
  FormatUnderlined,
  FormatColorText,
  FormatAlignLeft,
  FormatAlignCenter,
  FormatAlignRight,
  FormatAlignJustify,
  Save,
  Print,
  Undo,
  Redo,
  MoreVert,
  ArrowDropDown
} from '@mui/icons-material';

function DocumentToolbar() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));

// Format states
const [formats, setFormats] = useState([]);
const [alignment, setAlignment] = useState('left');

// Color menu state
const [colorAnchorEl, setColorAnchorEl] = useState(null);
const [selectedColor, setSelectedColor] = useState('black');

// Overflow menu state
const [overflowAnchorEl, setOverflowAnchorEl] = useState(null);

// Color options
const colors = [
{ name: 'Black', value: 'black' },
{ name: 'Red', value: '#f44336' },
{ name: 'Blue', value: '#2196f3' },
{ name: 'Green', value: '#4caf50' },
{ name: 'Purple', value: '#9c27b0' },
];

// Handle format changes
const handleFormatChange = (event, newFormats) => {
setFormats(newFormats);
};

// Handle alignment changes
const handleAlignmentChange = (event, newAlignment) => {
if (newAlignment !== null) {
setAlignment(newAlignment);
}
};

// Handle color menu
const handleColorClick = (event) => {
setColorAnchorEl(event.currentTarget);
};

const handleColorClose = () => {
setColorAnchorEl(null);
};

const handleColorSelect = (color) => {
setSelectedColor(color);
handleColorClose();
};

// Handle overflow menu
const handleOverflowClick = (event) => {
setOverflowAnchorEl(event.currentTarget);
};

const handleOverflowClose = () => {
setOverflowAnchorEl(null);
};

// Action handlers
const handleSave = () => {
console.log('Document saved');
};

const handlePrint = () => {
console.log('Document printed');
};

const handleUndo = () => {
console.log('Undo action');
};

const handleRedo = () => {
console.log('Redo action');
};

return (
<AppBar position="static" color="default" elevation={1}>
<Toolbar variant="dense" sx={{ flexWrap: 'wrap', gap: 1, py: 0.5 }}>
{/* History controls */}
<ButtonGroup size="small" aria-label="history controls">
<Button onClick={handleUndo}>
<Undo fontSize="small" />
{!isMobile && "Undo"}
</Button>
<Button onClick={handleRedo}>
<Redo fontSize="small" />
{!isMobile && "Redo"}
</Button>
</ButtonGroup>

        <Divider orientation="vertical" flexItem />

        {/* Text formatting */}
        <ToggleButtonGroup
          size="small"
          value={formats}
          onChange={handleFormatChange}
          aria-label="text formatting"
        >
          <ToggleButton value="bold" aria-label="bold">
            <FormatBold fontSize="small" />
          </ToggleButton>
          <ToggleButton value="italic" aria-label="italic">
            <FormatItalic fontSize="small" />
          </ToggleButton>
          <ToggleButton value="underlined" aria-label="underlined">
            <FormatUnderlined fontSize="small" />
          </ToggleButton>
        </ToggleButtonGroup>

        {/* Text color */}
        <Button
          size="small"
          startIcon={<FormatColorText />}
          endIcon={<ArrowDropDown />}
          onClick={handleColorClick}
          sx={{ color: selectedColor }}
        >
          {!isMobile && "Text Color"}
        </Button>
        <Menu
          anchorEl={colorAnchorEl}
          open={Boolean(colorAnchorEl)}
          onClose={handleColorClose}
        >
          {colors.map((color) => (
            <MenuItem
              key={color.value}
              onClick={() => handleColorSelect(color.value)}
              selected={selectedColor === color.value}
            >
              <ListItemIcon>
                <div style={{
                  width: 20,
                  height: 20,
                  backgroundColor: color.value,
                  borderRadius: '50%',
                  border: '1px solid #ddd'
                }} />
              </ListItemIcon>
              <ListItemText>{color.name}</ListItemText>
            </MenuItem>
          ))}
        </Menu>

        {!isMobile && (
          <>
            <Divider orientation="vertical" flexItem />

            {/* Text alignment */}
            <ToggleButtonGroup
              size="small"
              value={alignment}
              exclusive
              onChange={handleAlignmentChange}
              aria-label="text alignment"
            >
              <ToggleButton value="left" aria-label="left aligned">
                <FormatAlignLeft fontSize="small" />
              </ToggleButton>
              <ToggleButton value="center" aria-label="centered">
                <FormatAlignCenter fontSize="small" />
              </ToggleButton>
              <ToggleButton value="right" aria-label="right aligned">
                <FormatAlignRight fontSize="small" />
              </ToggleButton>
              <ToggleButton value="justify" aria-label="justified">
                <FormatAlignJustify fontSize="small" />
              </ToggleButton>
            </ToggleButtonGroup>
          </>
        )}

        {/* Spacer to push document actions to the right */}
        <div style={{ flexGrow: 1 }} />

        {/* Document actions */}
        {isMobile ? (
          <>
            <IconButton
              size="small"
              onClick={handleOverflowClick}
              aria-label="more options"
            >
              <MoreVert />
            </IconButton>
            <Menu
              anchorEl={overflowAnchorEl}
              open={Boolean(overflowAnchorEl)}
              onClose={handleOverflowClose}
            >
              <MenuItem onClick={handleSave}>
                <ListItemIcon>
                  <Save fontSize="small" />
                </ListItemIcon>
                <ListItemText>Save</ListItemText>
              </MenuItem>
              <MenuItem onClick={handlePrint}>
                <ListItemIcon>
                  <Print fontSize="small" />
                </ListItemIcon>
                <ListItemText>Print</ListItemText>
              </MenuItem>
              {isMobile && (
                <MenuItem onClick={() => {
                  setAlignment('left');
                  handleOverflowClose();
                }}>
                  <ListItemIcon>
                    <FormatAlignLeft fontSize="small" />
                  </ListItemIcon>
                  <ListItemText>Align Left</ListItemText>
                </MenuItem>
              )}
              {isMobile && (
                <MenuItem onClick={() => {
                  setAlignment('center');
                  handleOverflowClose();
                }}>
                  <ListItemIcon>
                    <FormatAlignCenter fontSize="small" />
                  </ListItemIcon>
                  <ListItemText>Align Center</ListItemText>
                </MenuItem>
              )}
              {isMobile && (
                <MenuItem onClick={() => {
                  setAlignment('right');
                  handleOverflowClose();
                }}>
                  <ListItemIcon>
                    <FormatAlignRight fontSize="small" />
                  </ListItemIcon>
                  <ListItemText>Align Right</ListItemText>
                </MenuItem>
              )}
              {isMobile && (
                <MenuItem onClick={() => {
                  setAlignment('justify');
                  handleOverflowClose();
                }}>
                  <ListItemIcon>
                    <FormatAlignJustify fontSize="small" />
                  </ListItemIcon>
                  <ListItemText>Justify</ListItemText>
                </MenuItem>
              )}
            </Menu>
          </>
        ) : (
          <ButtonGroup size="small" aria-label="document actions">
            <Button startIcon={<Save />} onClick={handleSave}>
              Save
            </Button>
            <Button startIcon={<Print />} onClick={handlePrint}>
              Print
            </Button>
          </ButtonGroup>
        )}
      </Toolbar>
    </AppBar>

);
}

export default DocumentToolbar;

This comprehensive toolbar example demonstrates several important concepts:

  1. Responsive design: The toolbar adapts to screen size, showing fewer controls and more compact options on mobile devices.
  2. Mixed control types: It combines ButtonGroup, ToggleButtonGroup, and standalone buttons to create a cohesive interface.
  3. Overflow handling: On small screens, less important actions are moved to an overflow menu.
  4. Visual organization: Dividers separate logical groups of controls.
  5. State management: Multiple states are tracked for different aspects of the document formatting.
  6. Menus for complex options: The color selector uses a dropdown menu to provide more options without taking up toolbar space.

This pattern can be adapted for many different types of applications, from document editors to image manipulation tools to data analysis interfaces.

Accessibility Considerations

When implementing ButtonGroup components, accessibility should be a primary concern. Here are some key considerations:

Proper ARIA Attributes

Always include appropriate ARIA attributes to ensure screen readers can properly interpret your interface:

import React from 'react';
import { ButtonGroup, Button } from '@mui/material';
import { FormatBold, FormatItalic, FormatUnderlined } from '@mui/icons-material';

function AccessibleButtonGroup() {
return (
<ButtonGroup 
      aria-label="text formatting options"
      variant="outlined"
    >
<Button aria-label="bold text">
<FormatBold />
</Button>
<Button aria-label="italic text">
<FormatItalic />
</Button>
<Button aria-label="underline text">
<FormatUnderlined />
</Button>
</ButtonGroup>
);
}

export default AccessibleButtonGroup;

Notice how we provide aria-label attributes at both the group level (describing the overall purpose) and the individual button level (describing each specific action).

Keyboard Navigation

Ensure your ButtonGroup components are fully navigable using the keyboard:

import React, { useState } from 'react';
import { ButtonGroup, Button, Box, Typography } from '@mui/material';

function KeyboardNavigableButtonGroup() {
const [activeButton, setActiveButton] = useState(null);

const handleButtonFocus = (index) => {
setActiveButton(index);
};

const handleButtonClick = (action) => {
console.log(`Performing action: ${action}`);
};

return (
<Box>
<Typography variant="body2" gutterBottom>
Use Tab to navigate between buttons, and Enter or Space to activate
</Typography>

      <ButtonGroup variant="contained" aria-label="document actions">
        <Button
          onFocus={() => handleButtonFocus(0)}
          onClick={() => handleButtonClick('save')}
          sx={{
            outline: activeButton === 0 ? '2px solid #90caf9' : 'none',
            zIndex: activeButton === 0 ? 1 : 'auto'
          }}
        >
          Save
        </Button>
        <Button
          onFocus={() => handleButtonFocus(1)}
          onClick={() => handleButtonClick('edit')}
          sx={{
            outline: activeButton === 1 ? '2px solid #90caf9' : 'none',
            zIndex: activeButton === 1 ? 1 : 'auto'
          }}
        >
          Edit
        </Button>
        <Button
          onFocus={() => handleButtonFocus(2)}
          onClick={() => handleButtonClick('delete')}
          sx={{
            outline: activeButton === 2 ? '2px solid #90caf9' : 'none',
            zIndex: activeButton === 2 ? 1 : 'auto'
          }}
        >
          Delete
        </Button>
      </ButtonGroup>
    </Box>

);
}

export default KeyboardNavigableButtonGroup;

This example enhances the default keyboard navigation by adding a more visible focus indicator, making it easier for keyboard users to see which button is currently focused.

Color Contrast

Ensure sufficient color contrast for all states of your buttons:

import React from 'react';
import { ButtonGroup, Button, createTheme, ThemeProvider } from '@mui/material';

function HighContrastButtonGroup() {
// Create a theme with enhanced contrast
const highContrastTheme = createTheme({
palette: {
primary: {
main: '#1a237e', // Darker blue for better contrast
},
error: {
main: '#b71c1c', // Darker red for better contrast
},
},
components: {
MuiButtonGroup: {
styleOverrides: {
grouped: {
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.12)', // More visible hover state
},
'&:focus': {
outline: '2px solid #90caf9', // More visible focus state
zIndex: 1,
},
},
},
},
MuiButton: {
styleOverrides: {
root: {
// Ensure text has good contrast against background
'&.MuiButton-contained': {
color: '#ffffff',
fontWeight: 500,
},
'&.MuiButton-outlined': {
fontWeight: 500,
},
},
},
},
},
});

return (
<ThemeProvider theme={highContrastTheme}>
<ButtonGroup variant="contained" aria-label="high contrast button group">
<Button>Accept</Button>
<Button>Modify</Button>
<Button color="error">Reject</Button>
</ButtonGroup>
</ThemeProvider>
);
}

export default HighContrastButtonGroup;

This example uses darker colors with better contrast ratios and enhances the visibility of interactive states like hover and focus.

Performance Optimization

When using ButtonGroup in larger applications, performance can become a concern. Here are some strategies to optimize performance:

Memoization for Stable Props

Use React's useMemo to prevent unnecessary re-renders:

import React, { useState, useMemo } from 'react';
import { ButtonGroup, Button } from '@mui/material';

function OptimizedButtonGroup() {
const [count, setCount] = useState(0);

// These button props won't change between renders
const buttonGroupProps = useMemo(() => ({
variant: 'contained',
size: 'small',
'aria-label': 'counter controls'
}), []);

return (
<div>
<p>Count: {count}</p>

      <ButtonGroup {...buttonGroupProps}>
        <Button onClick={() => setCount(count - 1)}>Decrease</Button>
        <Button onClick={() => setCount(0)}>Reset</Button>
        <Button onClick={() => setCount(count + 1)}>Increase</Button>
      </ButtonGroup>
    </div>

);
}

export default OptimizedButtonGroup;

By memoizing the props object, we prevent unnecessary prop comparisons and potential re-renders when the component updates for reasons unrelated to the ButtonGroup props.

Avoiding Inline Functions

Inline functions can cause unnecessary re-renders. Extract them to component-level functions:

import React, { useState, useCallback } from 'react';
import { ButtonGroup, Button } from '@mui/material';

function OptimizedCallbacks() {
const [count, setCount] = useState(0);

// Stable callback functions that won't change between renders
const handleDecrease = useCallback(() => {
setCount(prev => prev - 1);
}, []);

const handleReset = useCallback(() => {
setCount(0);
}, []);

const handleIncrease = useCallback(() => {
setCount(prev => prev + 1);
}, []);

return (
<div>
<p>Count: {count}</p>

      <ButtonGroup variant="contained" size="small" aria-label="counter controls">
        <Button onClick={handleDecrease}>Decrease</Button>
        <Button onClick={handleReset}>Reset</Button>
        <Button onClick={handleIncrease}>Increase</Button>
      </ButtonGroup>
    </div>

);
}

export default OptimizedCallbacks;

By using useCallback, we ensure that the function references remain stable between renders, which can help prevent unnecessary re-renders of child components.

Common Issues and Solutions

Here are some common issues you might encounter when working with ButtonGroup, along with their solutions:

Inconsistent Button Sizes

Sometimes buttons in a group may have inconsistent widths, especially when they contain different amounts of text:

import React from 'react';
import { ButtonGroup, Button } from '@mui/material';

function EqualWidthButtonGroup() {
return (
<ButtonGroup
variant="contained"
aria-label="equal width button group"
sx={{
        '& .MuiButton-root': {
          flex: 1,
          minWidth: 100, // Ensure a minimum width
        },
      }} >
<Button>OK</Button>
<Button>Cancel</Button>
<Button>Apply Changes</Button>
</ButtonGroup>
);
}

export default EqualWidthButtonGroup;

This solution uses the flex: 1 property to make all buttons take up equal space within the group, with a minimum width to ensure very short text doesn't result in too-narrow buttons.

Border Issues Between Buttons

Sometimes the borders between buttons in a group can appear doubled or inconsistent:

import React from 'react';
import { ButtonGroup, Button, createTheme, ThemeProvider } from '@mui/material';

function FixedBorderButtonGroup() {
const theme = createTheme({
components: {
MuiButtonGroup: {
styleOverrides: {
grouped: {
'&:not(:last-of-type)': {
borderRight: '1px solid rgba(0, 0, 0, 0.12)',
},
// Ensure no double borders
borderRight: 'none',
},
},
},
},
});

return (
<ThemeProvider theme={theme}>
<ButtonGroup variant="outlined" aria-label="fixed border button group">
<Button>Left</Button>
<Button>Middle</Button>
<Button>Right</Button>
</ButtonGroup>
</ThemeProvider>
);
}

export default FixedBorderButtonGroup;

This solution uses theme overrides to ensure consistent border rendering between buttons in the group.

Accessibility with Icon-Only Buttons

Icon-only buttons can be problematic for accessibility if not properly labeled:

import React from 'react';
import { ButtonGroup, Button, Tooltip } from '@mui/material';
import { FormatBold, FormatItalic, FormatUnderlined } from '@mui/icons-material';

function AccessibleIconButtonGroup() {
return (
<ButtonGroup variant="outlined" aria-label="text formatting">
<Tooltip title="Bold">
<Button aria-label="bold text">
<FormatBold />
</Button>
</Tooltip>
<Tooltip title="Italic">
<Button aria-label="italic text">
<FormatItalic />
</Button>
</Tooltip>
<Tooltip title="Underline">
<Button aria-label="underline text">
<FormatUnderlined />
</Button>
</Tooltip>
</ButtonGroup>
);
}

export default AccessibleIconButtonGroup;

This solution combines aria-label attributes with tooltips to provide both screen reader support and visual cues for sighted users who may not recognize the icons.

Wrapping Up

The ButtonGroup component is a powerful tool in the MUI arsenal that helps create cohesive, visually connected action controls. When used effectively, it can significantly improve the user experience by clearly indicating related actions and providing a more organized interface.

Throughout this guide, we've explored everything from basic usage to advanced patterns, customization techniques, accessibility considerations, and performance optimizations. By applying these principles, you can create intuitive, accessible, and visually appealing action controls that enhance your React applications.

Remember that ButtonGroup works best when the grouped buttons are truly related actions—overusing it can lead to visual clutter and confusion. When in doubt, ask whether the actions you're grouping are conceptually related enough to warrant visual grouping. When they are, ButtonGroup provides an elegant, built-in solution that maintains Material Design principles while offering extensive customization options.