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:
Prop | Type | Default | Description |
---|---|---|---|
children | node | - | The content of the component, typically Button elements |
color | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' | string | 'primary' | The color of the component |
disabled | bool | false | If true, all buttons will be disabled |
disableElevation | bool | false | If true, no elevation is applied to the buttons |
disableRipple | bool | false | If true, the ripple effect is disabled for all buttons |
fullWidth | bool | false | 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 |
sx | object | - | 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:
- Outlined (default): Buttons with a border outline and transparent background
- Contained: Solid buttons with background color and elevation
- 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:
- Using ButtonGroup to organize related controls (text formatting in one group, alignment in another)
- Managing state to track which formatting options are active
- Visually indicating active state by changing button colors
- Using icons for better visual recognition
- Separating button groups with dividers for logical grouping
- 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:
- Consistent styling across the application
- Separation of styling from component logic
- The ability to define variants that can be reused
- 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:
- Responsive design: The toolbar adapts to screen size, showing fewer controls and more compact options on mobile devices.
- Mixed control types: It combines ButtonGroup, ToggleButtonGroup, and standalone buttons to create a cohesive interface.
- Overflow handling: On small screens, less important actions are moved to an overflow menu.
- Visual organization: Dividers separate logical groups of controls.
- State management: Multiple states are tracked for different aspects of the document formatting.
- 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.