Building an Action Toolbar with React MUI Icons: A Complete Guide
When building modern React applications, creating intuitive and responsive UI components is essential. Action toolbars are among the most common UI patterns you'll implement, appearing in everything from document editors to admin dashboards. In this guide, I'll walk you through building a powerful, customizable action toolbar using Material UI (MUI) icons and components.
Having built countless toolbars over the years, I've found that MUI provides the perfect balance of flexibility and structure for these interface elements. Let's dive into creating a professional-grade action toolbar that your users will love.
Learning Objectives
By the end of this tutorial, you'll be able to:
- Set up and configure a responsive action toolbar using MUI components
- Implement and customize various icon buttons with proper accessibility attributes
- Create different toolbar variants (horizontal, vertical, compact)
- Handle icon button states, tooltips, and click events
- Apply custom styling and theming to your toolbar
- Implement advanced features like keyboard navigation and conditional rendering
Understanding MUI Icons and Their Role in Toolbars
Material UI offers an extensive collection of icons through the @mui/icons-material
package. These icons follow Google's Material Design guidelines, ensuring consistency across your application while providing visual cues that users intuitively understand.
Action toolbars typically consist of a row or column of icon buttons that trigger specific functions. Think of the formatting toolbar in Google Docs or the action bar in your email client. These toolbars need to be compact yet accessible, with clear visual feedback and proper keyboard support.
MUI's icon system works seamlessly with its Button and IconButton components, making it ideal for building such interfaces. The icons themselves are React components that can be customized with props like fontSize
, color
, and custom styling via the sx
prop.
Setting Up Your Project
Before we start building our toolbar, let's set up a basic React project with MUI installed. If you already have a React project, you can skip to the installation steps.
Installing the Required Packages
We'll need several MUI packages to build our toolbar:
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
If you prefer using Yarn:
yarn add @mui/material @mui/icons-material @emotion/react @emotion/styled
Creating a Basic Project Structure
Let's organize our project with a component-based structure. We'll create separate components for our toolbar and demonstrate how to use it in a main App component.
src/
├── components/
│ ├── ActionToolbar.jsx
│ └── ActionButton.jsx
├── App.jsx
└── index.js
MUI IconButton Component Deep Dive
The IconButton component is the foundation of our action toolbar. It's specifically designed to contain icons and provides appropriate padding, touch targets, and ripple effects.
IconButton Props
The IconButton component accepts numerous props that allow for extensive customization:
Prop | Type | Default | Description |
---|---|---|---|
color | string | 'default' | The color of the component. Can be 'primary', 'secondary', 'error', 'info', 'success', 'warning', or any custom color defined in your theme. |
disabled | boolean | false | If true, the button will be disabled. |
disableFocusRipple | boolean | false | If true, the keyboard focus ripple will be disabled. |
disableRipple | boolean | false | If true, the ripple effect will be disabled. |
edge | string | false | If given, the button will use additional styles for edge positions. Options are 'start', 'end', or false. |
size | string | 'medium' | The size of the button. Can be 'small', 'medium', or 'large'. |
sx | object | The system prop that allows defining system overrides as well as additional CSS styles. |
Customization Options
IconButton can be customized in several ways:
- Through props: Basic styling like color and size can be controlled via props.
- Using the sx prop: For more specific styling needs, the sx prop provides access to the theme and accepts CSS properties.
- Theme customization: You can override the default styles in your theme.
- Styled components: Using the styled API to create custom styled versions.
Here's an example of customizing an IconButton:
import { IconButton } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
// Basic usage
<IconButton>
<DeleteIcon />
</IconButton>
// With color and size props
<IconButton color="primary" size="large">
<DeleteIcon />
</IconButton>
// With sx prop for custom styling
<IconButton
sx={{
bgcolor: 'background.paper',
'&:hover': {
bgcolor: 'primary.light',
},
borderRadius: 1
}}
>
<DeleteIcon />
</IconButton>
Accessibility Considerations
When using IconButtons, accessibility is crucial since icons alone may not convey meaning to all users. MUI's IconButton is designed with accessibility in mind, but you should:
- Always provide an
aria-label
for buttons without text - Use the
Tooltip
component to provide additional context - Ensure proper focus states and keyboard navigation
- Consider high contrast and color blindness when choosing icons and colors
Building Our Action Button Component
Let's start by creating a reusable ActionButton component that we'll use in our toolbar. This component will wrap MUI's IconButton with additional features like tooltips and consistent styling.
import React from 'react';
import { IconButton, Tooltip } from '@mui/material';
const ActionButton = ({
icon,
label,
onClick,
disabled = false,
color = 'default',
size = 'medium',
tooltipPlacement = 'bottom',
sx = {},
...props
}) => {
return (
<Tooltip title={label} placement={tooltipPlacement} arrow>
<span>
<IconButton
onClick={onClick}
disabled={disabled}
color={color}
size={size}
aria-label={label}
sx={{
m: 0.5,
...sx
}}
{...props}
>
{icon}
</IconButton>
</span>
</Tooltip>
);
};
export default ActionButton;
In this component:
- We wrap the IconButton with a Tooltip to provide context to users
- We use a
span
wrapper around the IconButton to ensure the tooltip works even when the button is disabled - We pass through essential props like color, size, and onClick
- We set an aria-label for accessibility
- We apply consistent margin with the ability to override via the sx prop
Creating the Action Toolbar Component
Now, let's build our main ActionToolbar component that will house multiple action buttons:
import React from 'react';
import { Paper, Stack, Divider } from '@mui/material';
import ActionButton from './ActionButton';
const ActionToolbar = ({
actions,
direction = 'row',
spacing = 1,
dividers = false,
variant = 'outlined',
color = 'default',
size = 'medium',
sx = {},
...props
}) => {
return (
<Paper
variant={variant}
sx={{
display: 'inline-flex',
p: 0.5,
...sx
}}
{...props}
>
<Stack
direction={direction}
spacing={spacing}
divider={dividers ? <Divider orientation={direction === 'row' ? 'vertical' : 'horizontal'} flexItem /> : null}
alignItems="center"
>
{actions.map((action, index) => (
<ActionButton
key={action.id || index}
icon={action.icon}
label={action.label}
onClick={action.onClick}
disabled={action.disabled}
color={action.color || color}
size={action.size || size}
tooltipPlacement={direction === 'row' ? 'bottom' : 'right'}
sx={action.sx}
/>
))}
</Stack>
</Paper>
);
};
export default ActionToolbar;
This component:
- Uses a Paper component as a container for the toolbar
- Implements a Stack component to arrange the buttons either horizontally or vertically
- Supports optional dividers between buttons
- Allows for global styling and individual button customization
- Accepts an array of action objects that define each button's properties
Creating a Complete Toolbar Example
Let's put everything together to create a document editing toolbar example:
import React, { useState } from 'react';
import { Box, Typography, useTheme } from '@mui/material';
import ActionToolbar from './components/ActionToolbar';
// Import icons
import FormatBoldIcon from '@mui/icons-material/FormatBold';
import FormatItalicIcon from '@mui/icons-material/FormatItalic';
import FormatUnderlinedIcon from '@mui/icons-material/FormatUnderlined';
import FormatAlignLeftIcon from '@mui/icons-material/FormatAlignLeft';
import FormatAlignCenterIcon from '@mui/icons-material/FormatAlignCenter';
import FormatAlignRightIcon from '@mui/icons-material/FormatAlignRight';
import UndoIcon from '@mui/icons-material/Undo';
import RedoIcon from '@mui/icons-material/Redo';
import SaveIcon from '@mui/icons-material/Save';
import DeleteIcon from '@mui/icons-material/Delete';
const App = () => {
const theme = useTheme();
// State for toggle buttons
const [formatState, setFormatState] = useState({
bold: false,
italic: false,
underline: false,
});
const [alignState, setAlignState] = useState('left');
// Handler for format buttons
const handleFormatClick = (format) => {
setFormatState(prev => ({
...prev,
[format]: !prev[format]
}));
};
// Handler for alignment buttons
const handleAlignClick = (alignment) => {
setAlignState(alignment);
};
// Define actions for the text formatting toolbar
const formatActions = [
{
id: 'bold',
icon: <FormatBoldIcon />,
label: 'Bold',
onClick: () => handleFormatClick('bold'),
color: formatState.bold ? 'primary' : 'default',
sx: formatState.bold ? { bgcolor: theme.palette.action.selected } : {}
},
{
id: 'italic',
icon: <FormatItalicIcon />,
label: 'Italic',
onClick: () => handleFormatClick('italic'),
color: formatState.italic ? 'primary' : 'default',
sx: formatState.italic ? { bgcolor: theme.palette.action.selected } : {}
},
{
id: 'underline',
icon: <FormatUnderlinedIcon />,
label: 'Underline',
onClick: () => handleFormatClick('underline'),
color: formatState.underline ? 'primary' : 'default',
sx: formatState.underline ? { bgcolor: theme.palette.action.selected } : {}
},
];
// Define actions for the alignment toolbar
const alignActions = [
{
id: 'align-left',
icon: <FormatAlignLeftIcon />,
label: 'Align Left',
onClick: () => handleAlignClick('left'),
color: alignState === 'left' ? 'primary' : 'default',
sx: alignState === 'left' ? { bgcolor: theme.palette.action.selected } : {}
},
{
id: 'align-center',
icon: <FormatAlignCenterIcon />,
label: 'Align Center',
onClick: () => handleAlignClick('center'),
color: alignState === 'center' ? 'primary' : 'default',
sx: alignState === 'center' ? { bgcolor: theme.palette.action.selected } : {}
},
{
id: 'align-right',
icon: <FormatAlignRightIcon />,
label: 'Align Right',
onClick: () => handleAlignClick('right'),
color: alignState === 'right' ? 'primary' : 'default',
sx: alignState === 'right' ? { bgcolor: theme.palette.action.selected } : {}
},
];
// Define actions for the document toolbar
const documentActions = [
{
id: 'undo',
icon: <UndoIcon />,
label: 'Undo',
onClick: () => console.log('Undo clicked')
},
{
id: 'redo',
icon: <RedoIcon />,
label: 'Redo',
onClick: () => console.log('Redo clicked')
},
{
id: 'save',
icon: <SaveIcon />,
label: 'Save',
onClick: () => console.log('Save clicked'),
color: 'primary'
},
{
id: 'delete',
icon: <DeleteIcon />,
label: 'Delete',
onClick: () => console.log('Delete clicked'),
color: 'error'
},
];
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Document Editor Toolbar Example
</Typography>
<Box sx={{ mb: 4 }}>
<Typography variant="h6" gutterBottom>
Horizontal Toolbar with Dividers
</Typography>
<ActionToolbar
actions={[...formatActions, ...alignActions, ...documentActions]}
dividers={true}
/>
</Box>
<Box sx={{ mb: 4 }}>
<Typography variant="h6" gutterBottom>
Grouped Toolbars
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<ActionToolbar actions={formatActions} />
<ActionToolbar actions={alignActions} />
<ActionToolbar actions={documentActions} />
</Box>
</Box>
<Box sx={{ mb: 4, display: 'flex', gap: 2 }}>
<Box>
<Typography variant="h6" gutterBottom>
Vertical Toolbar
</Typography>
<ActionToolbar
actions={documentActions}
direction="column"
variant="elevation"
sx={{ boxShadow: 2 }}
/>
</Box>
<Box sx={{ ml: 4 }}>
<Typography variant="h6" gutterBottom>
Toolbar with Custom Styling
</Typography>
<ActionToolbar
actions={formatActions}
sx={{
bgcolor: theme.palette.primary.light,
borderRadius: 2,
'& .MuiIconButton-root': {
color: theme.palette.primary.contrastText,
'&:hover': {
bgcolor: theme.palette.primary.main,
}
}
}}
/>
</Box>
</Box>
</Box>
);
};
export default App;
This example demonstrates:
- A complete document editor toolbar with formatting, alignment, and document actions
- Toggle buttons that maintain state (bold, italic, underline, alignment)
- Different toolbar layouts (horizontal, vertical, grouped)
- Custom styling options
- Proper handling of button states and visual feedback
Step-by-Step Guide to Building a Custom Action Toolbar
Let's break down the process of building a custom action toolbar from scratch, explaining each step in detail:
Step 1: Define Your Toolbar Requirements
Before writing any code, determine what your toolbar needs to accomplish:
- What actions will it contain?
- How should it be laid out (horizontal, vertical, grouped)?
- Do you need state management for toggle buttons?
- What kind of styling and theming requirements do you have?
For our example, we'll build a simplified file management toolbar with upload, download, share, and delete actions.
Step 2: Set Up the Project Structure
Create a new React component for your toolbar:
import React from 'react';
import { Box } from '@mui/material';
const FileToolbar = () => {
return (
<Box>
{/* Toolbar content will go here */}
</Box>
);
};
export default FileToolbar;
Step 3: Install and Import Required MUI Components
Make sure you have the necessary MUI packages installed, then import the components and icons you'll need:
import React from 'react';
import { Box, IconButton, Tooltip, Paper } from '@mui/material';
// Import icons
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
import ShareIcon from '@mui/icons-material/Share';
import DeleteIcon from '@mui/icons-material/Delete';
Step 4: Create the Basic Toolbar Structure
Build the container for your toolbar using MUI's Paper component:
import React from 'react';
import { Box, IconButton, Tooltip, Paper } from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
import ShareIcon from '@mui/icons-material/Share';
import DeleteIcon from '@mui/icons-material/Delete';
const FileToolbar = () => {
return (
<Paper
elevation={1}
sx={{
display: 'inline-flex',
p: 0.5,
borderRadius: 1,
}}
>
{/* Icon buttons will go here */}
</Paper>
);
};
export default FileToolbar;
The Paper component provides a nice container with a subtle elevation and background color, making the toolbar stand out from the rest of the interface.
Step 5: Add Icon Buttons with Tooltips
Now, let's add the action buttons with tooltips for accessibility:
import React from 'react';
import { Box, IconButton, Tooltip, Paper } from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
import ShareIcon from '@mui/icons-material/Share';
import DeleteIcon from '@mui/icons-material/Delete';
const FileToolbar = () => {
// Define click handlers
const handleUpload = () => {
console.log('Upload clicked');
// Implement your upload logic here
};
const handleDownload = () => {
console.log('Download clicked');
// Implement your download logic here
};
const handleShare = () => {
console.log('Share clicked');
// Implement your share logic here
};
const handleDelete = () => {
console.log('Delete clicked');
// Implement your delete logic here
};
return (
<Paper
elevation={1}
sx={{
display: 'inline-flex',
p: 0.5,
borderRadius: 1,
}}
>
<Tooltip title="Upload file" arrow>
<IconButton
onClick={handleUpload}
aria-label="Upload file"
color="primary"
size="medium"
sx={{ m: 0.5 }}
>
<CloudUploadIcon />
</IconButton>
</Tooltip>
<Tooltip title="Download file" arrow>
<IconButton
onClick={handleDownload}
aria-label="Download file"
color="primary"
size="medium"
sx={{ m: 0.5 }}
>
<CloudDownloadIcon />
</IconButton>
</Tooltip>
<Tooltip title="Share file" arrow>
<IconButton
onClick={handleShare}
aria-label="Share file"
color="primary"
size="medium"
sx={{ m: 0.5 }}
>
<ShareIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete file" arrow>
<IconButton
onClick={handleDelete}
aria-label="Delete file"
color="error"
size="medium"
sx={{ m: 0.5 }}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</Paper>
);
};
export default FileToolbar;
In this step, we've:
- Added IconButton components for each action
- Wrapped each button in a Tooltip for accessibility and user guidance
- Set appropriate aria-labels for screen readers
- Defined click handlers for each action
- Used different colors to indicate different types of actions (primary for standard actions, error for destructive actions)
Step 6: Make the Toolbar More Reusable with Props
Let's refactor our component to make it more reusable by accepting props:
import React from 'react';
import { IconButton, Tooltip, Paper } from '@mui/material';
const FileToolbar = ({
actions,
elevation = 1,
variant = 'elevation',
sx = {},
...props
}) => {
return (
<Paper
elevation={variant === 'elevation' ? elevation : 0}
variant={variant}
sx={{
display: 'inline-flex',
p: 0.5,
borderRadius: 1,
...sx
}}
{...props}
>
{actions.map((action) => (
<Tooltip key={action.id} title={action.tooltip} arrow>
<IconButton
onClick={action.onClick}
aria-label={action.tooltip}
color={action.color || 'primary'}
size={action.size || 'medium'}
disabled={action.disabled}
sx={{ m: 0.5, ...(action.sx || {}) }}
>
{action.icon}
</IconButton>
</Tooltip>
))}
</Paper>
);
};
export default FileToolbar;
Now our toolbar accepts an array of action objects, making it much more flexible and reusable.
Step 7: Implement the Toolbar in a Parent Component
Let's see how we would use this reusable toolbar in a parent component:
import React from 'react';
import { Box, Typography } from '@mui/material';
import FileToolbar from './FileToolbar';
// Import icons
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
import ShareIcon from '@mui/icons-material/Share';
import DeleteIcon from '@mui/icons-material/Delete';
const FileManager = () => {
// Define file actions
const fileActions = [
{
id: 'upload',
tooltip: 'Upload file',
icon: <CloudUploadIcon />,
onClick: () => console.log('Upload clicked'),
color: 'primary'
},
{
id: 'download',
tooltip: 'Download file',
icon: <CloudDownloadIcon />,
onClick: () => console.log('Download clicked'),
color: 'primary',
// Example of a disabled button
disabled: false
},
{
id: 'share',
tooltip: 'Share file',
icon: <ShareIcon />,
onClick: () => console.log('Share clicked'),
color: 'primary'
},
{
id: 'delete',
tooltip: 'Delete file',
icon: <DeleteIcon />,
onClick: () => console.log('Delete clicked'),
color: 'error'
},
];
return (
<Box sx={{ p: 3 }}>
<Typography variant="h5" gutterBottom>
File Manager
</Typography>
<FileToolbar actions={fileActions} />
</Box>
);
};
export default FileManager;
Step 8: Add Responsive Behavior
Let's enhance our toolbar to be responsive, adapting to different screen sizes:
import React from 'react';
import { IconButton, Tooltip, Paper, useMediaQuery, useTheme } from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
const FileToolbar = ({
actions,
elevation = 1,
variant = 'elevation',
sx = {},
responsiveBreakpoint = 'sm',
...props
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(responsiveBreakpoint));
// State for the overflow menu
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
const handleMenuOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
// Decide which actions to show directly and which to put in the overflow menu
const visibleActions = isMobile ? actions.slice(0, 2) : actions;
const overflowActions = isMobile ? actions.slice(2) : [];
return (
<Paper
elevation={variant === 'elevation' ? elevation : 0}
variant={variant}
sx={{
display: 'inline-flex',
p: 0.5,
borderRadius: 1,
...sx
}}
{...props}
>
{/* Visible actions */}
{visibleActions.map((action) => (
<Tooltip key={action.id} title={action.tooltip} arrow>
<IconButton
onClick={action.onClick}
aria-label={action.tooltip}
color={action.color || 'primary'}
size={action.size || 'medium'}
disabled={action.disabled}
sx={{ m: 0.5, ...(action.sx || {}) }}
>
{action.icon}
</IconButton>
</Tooltip>
))}
{/* Overflow menu for mobile */}
{isMobile && overflowActions.length > 0 && (
<>
<Tooltip title="More actions" arrow>
<IconButton
onClick={handleMenuOpen}
aria-label="More actions"
aria-controls={open ? 'overflow-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
sx={{ m: 0.5 }}
>
<MoreVertIcon />
</IconButton>
</Tooltip>
<Menu
id="overflow-menu"
anchorEl={anchorEl}
open={open}
onClose={handleMenuClose}
MenuListProps={{
'aria-labelledby': 'overflow-button',
}}
>
{overflowActions.map((action) => (
<MenuItem
key={action.id}
onClick={() => {
action.onClick();
handleMenuClose();
}}
disabled={action.disabled}
>
<ListItemIcon sx={{ color: action.color === 'error' ? 'error.main' : 'primary.main' }}>
{action.icon}
</ListItemIcon>
<ListItemText>{action.tooltip}</ListItemText>
</MenuItem>
))}
</Menu>
</>
)}
</Paper>
);
};
export default FileToolbar;
In this enhanced version:
- We use MUI's
useMediaQuery
hook to detect screen size - On smaller screens, we show only the first two actions directly
- The remaining actions are placed in an overflow menu accessed via a "more" icon
- The menu displays the actions with both icons and text for better usability
Step 9: Add Keyboard Navigation Support
Let's enhance our toolbar with keyboard navigation support:
import React, { useRef, useState } from 'react';
import { IconButton, Tooltip, Paper, useMediaQuery, useTheme } from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
const FileToolbar = ({
actions,
elevation = 1,
variant = 'elevation',
sx = {},
responsiveBreakpoint = 'sm',
...props
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(responsiveBreakpoint));
// State for the overflow menu
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
// Refs for keyboard navigation
const buttonRefs = useRef([]);
const [focusIndex, setFocusIndex] = useState(-1);
const handleMenuOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
// Keyboard navigation handlers
const handleKeyDown = (event) => {
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
event.preventDefault();
const nextIndex = (focusIndex + 1) % buttonRefs.current.length;
buttonRefs.current[nextIndex]?.focus();
setFocusIndex(nextIndex);
} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
event.preventDefault();
const prevIndex = focusIndex <= 0 ? buttonRefs.current.length - 1 : focusIndex - 1;
buttonRefs.current[prevIndex]?.focus();
setFocusIndex(prevIndex);
}
};
// Decide which actions to show directly and which to put in the overflow menu
const visibleActions = isMobile ? actions.slice(0, 2) : actions;
const overflowActions = isMobile ? actions.slice(2) : [];
return (
<Paper
elevation={variant === 'elevation' ? elevation : 0}
variant={variant}
sx={{
display: 'inline-flex',
p: 0.5,
borderRadius: 1,
...sx
}}
role="toolbar"
aria-label="File actions"
onKeyDown={handleKeyDown}
{...props}
>
{/* Visible actions */}
{visibleActions.map((action, index) => (
<Tooltip key={action.id} title={action.tooltip} arrow>
<IconButton
ref={(el) => buttonRefs.current[index] = el}
onClick={action.onClick}
aria-label={action.tooltip}
color={action.color || 'primary'}
size={action.size || 'medium'}
disabled={action.disabled}
sx={{ m: 0.5, ...(action.sx || {}) }}
tabIndex={focusIndex === index ? 0 : -1}
onFocus={() => setFocusIndex(index)}
>
{action.icon}
</IconButton>
</Tooltip>
))}
{/* Overflow menu for mobile */}
{isMobile && overflowActions.length > 0 && (
<>
<Tooltip title="More actions" arrow>
<IconButton
ref={(el) => buttonRefs.current[visibleActions.length] = el}
onClick={handleMenuOpen}
aria-label="More actions"
aria-controls={open ? 'overflow-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
sx={{ m: 0.5 }}
tabIndex={focusIndex === visibleActions.length ? 0 : -1}
onFocus={() => setFocusIndex(visibleActions.length)}
>
<MoreVertIcon />
</IconButton>
</Tooltip>
<Menu
id="overflow-menu"
anchorEl={anchorEl}
open={open}
onClose={handleMenuClose}
MenuListProps={{
'aria-labelledby': 'overflow-button',
}}
>
{overflowActions.map((action) => (
<MenuItem
key={action.id}
onClick={() => {
action.onClick();
handleMenuClose();
}}
disabled={action.disabled}
>
<ListItemIcon sx={{ color: action.color === 'error' ? 'error.main' : 'primary.main' }}>
{action.icon}
</ListItemIcon>
<ListItemText>{action.tooltip}</ListItemText>
</MenuItem>
))}
</Menu>
</>
)}
</Paper>
);
};
export default FileToolbar;
This enhanced version adds:
- Proper keyboard navigation using arrow keys
- Focus management with tabIndex
- Appropriate ARIA roles and labels
- A consistent focus system that works across all toolbar buttons
Advanced Toolbar Capabilities
Let's explore some advanced capabilities for our action toolbar:
Creating Toggle Buttons
Toggle buttons are essential for actions that can be turned on or off, like text formatting options:
import React, { useState } from 'react';
import { Box, Typography } from '@mui/material';
import FormatBoldIcon from '@mui/icons-material/FormatBold';
import FormatItalicIcon from '@mui/icons-material/FormatItalic';
import FormatUnderlinedIcon from '@mui/icons-material/FormatUnderlined';
import FileToolbar from './FileToolbar';
const TextEditor = () => {
const [textFormat, setTextFormat] = useState({
bold: false,
italic: false,
underline: false
});
const handleFormatToggle = (format) => {
setTextFormat(prev => ({
...prev,
[format]: !prev[format]
}));
};
const formatActions = [
{
id: 'bold',
tooltip: 'Bold',
icon: <FormatBoldIcon />,
onClick: () => handleFormatToggle('bold'),
color: textFormat.bold ? 'primary' : 'default',
sx: textFormat.bold ? {
bgcolor: 'action.selected',
'&:hover': { bgcolor: 'action.selected' }
} : {}
},
{
id: 'italic',
tooltip: 'Italic',
icon: <FormatItalicIcon />,
onClick: () => handleFormatToggle('italic'),
color: textFormat.italic ? 'primary' : 'default',
sx: textFormat.italic ? {
bgcolor: 'action.selected',
'&:hover': { bgcolor: 'action.selected' }
} : {}
},
{
id: 'underline',
tooltip: 'Underline',
icon: <FormatUnderlinedIcon />,
onClick: () => handleFormatToggle('underline'),
color: textFormat.underline ? 'primary' : 'default',
sx: textFormat.underline ? {
bgcolor: 'action.selected',
'&:hover': { bgcolor: 'action.selected' }
} : {}
}
];
return (
<Box sx={{ p: 3 }}>
<Typography variant="h5" gutterBottom>
Text Editor
</Typography>
<FileToolbar
actions={formatActions}
variant="outlined"
/>
<Box sx={{
mt: 2,
p: 2,
border: '1px solid #ccc',
borderRadius: 1,
minHeight: 100,
fontWeight: textFormat.bold ? 'bold' : 'normal',
fontStyle: textFormat.italic ? 'italic' : 'normal',
textDecoration: textFormat.underline ? 'underline' : 'none'
}}>
Type your text here...
</Box>
</Box>
);
};
export default TextEditor;
This example demonstrates:
- Using state to track toggle button status
- Visually indicating active state with background color and text color
- Applying the formatting to the content area based on the toggle state
Creating Grouped Toolbars
For complex interfaces, grouping related actions can improve usability:
import React, { useState } from 'react';
import { Box, Typography, Divider } from '@mui/material';
import FileToolbar from './FileToolbar';
// Import file operation icons
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
import ShareIcon from '@mui/icons-material/Share';
import DeleteIcon from '@mui/icons-material/Delete';
// Import editing icons
import ContentCutIcon from '@mui/icons-material/ContentCut';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
// Import view icons
import ZoomInIcon from '@mui/icons-material/ZoomIn';
import ZoomOutIcon from '@mui/icons-material/ZoomOut';
import FullscreenIcon from '@mui/icons-material/Fullscreen';
const DocumentEditor = () => {
// Define file actions
const fileActions = [
{
id: 'upload',
tooltip: 'Upload document',
icon: <CloudUploadIcon />,
onClick: () => console.log('Upload clicked')
},
{
id: 'download',
tooltip: 'Download document',
icon: <CloudDownloadIcon />,
onClick: () => console.log('Download clicked')
},
{
id: 'share',
tooltip: 'Share document',
icon: <ShareIcon />,
onClick: () => console.log('Share clicked')
},
{
id: 'delete',
tooltip: 'Delete document',
icon: <DeleteIcon />,
onClick: () => console.log('Delete clicked'),
color: 'error'
}
];
// Define edit actions
const editActions = [
{
id: 'cut',
tooltip: 'Cut',
icon: <ContentCutIcon />,
onClick: () => console.log('Cut clicked')
},
{
id: 'copy',
tooltip: 'Copy',
icon: <ContentCopyIcon />,
onClick: () => console.log('Copy clicked')
},
{
id: 'paste',
tooltip: 'Paste',
icon: <ContentPasteIcon />,
onClick: () => console.log('Paste clicked')
}
];
// Define view actions
const viewActions = [
{
id: 'zoom-in',
tooltip: 'Zoom in',
icon: <ZoomInIcon />,
onClick: () => console.log('Zoom in clicked')
},
{
id: 'zoom-out',
tooltip: 'Zoom out',
icon: <ZoomOutIcon />,
onClick: () => console.log('Zoom out clicked')
},
{
id: 'fullscreen',
tooltip: 'Fullscreen',
icon: <FullscreenIcon />,
onClick: () => console.log('Fullscreen clicked')
}
];
return (
<Box sx={{ p: 3 }}>
<Typography variant="h5" gutterBottom>
Document Editor
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<FileToolbar
actions={fileActions}
variant="outlined"
sx={{ borderColor: 'primary.light' }}
/>
<FileToolbar
actions={editActions}
variant="outlined"
sx={{ borderColor: 'secondary.light' }}
/>
<FileToolbar
actions={viewActions}
variant="outlined"
sx={{ borderColor: 'info.light' }}
/>
</Box>
<Divider sx={{ my: 2 }} />
<Typography variant="h6" gutterBottom>
Combined Toolbar with Sections
</Typography>
<FileToolbar
actions={[
...fileActions,
{ id: 'divider-1', divider: true },
...editActions,
{ id: 'divider-2', divider: true },
...viewActions
]}
variant="elevation"
elevation={3}
sx={{
borderRadius: 2,
p: 1
}}
renderAction={(action) => {
if (action.divider) {
return <Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />;
}
return null; // Default rendering
}}
/>
</Box>
);
};
export default DocumentEditor;
In this example, we've:
- Created separate toolbars for different categories of actions
- Used different border colors to visually distinguish the groups
- Created a combined toolbar with dividers between sections
- Added a custom renderAction prop to handle special items like dividers
Best Practices for MUI Icon Toolbars
Based on years of experience building UI components, here are some best practices for creating effective action toolbars:
Accessibility
- Always provide tooltips: Every icon button should have a descriptive tooltip
- Use aria-labels: Icon-only buttons need proper aria-labels for screen readers
- Ensure keyboard navigation: Users should be able to navigate the toolbar using only the keyboard
- Test with screen readers: Verify that your toolbar works with popular screen readers
Visual Design
- Maintain consistent spacing: Use consistent margins between buttons
- Use color purposefully: Reserve accent colors for important actions or active states
- Provide visual feedback: Buttons should have hover, focus, and active states
- Group related actions: Use dividers or separate toolbars for different categories of actions
Responsiveness
- Adapt to screen size: Show fewer buttons on small screens
- Use overflow menus: Move less important actions to a dropdown menu on mobile
- Consider touch targets: Ensure buttons are large enough for touch on mobile devices
- Test on real devices: Verify that your toolbar works well on various screen sizes
Performance
- Memoize event handlers: Use
useCallback
for event handlers to prevent unnecessary re-renders - Lazy load icons: If you're using many icons, consider loading them dynamically
- Minimize state updates: Batch state updates when possible to reduce renders
- Use React.memo: Wrap your toolbar component with React.memo if it doesn't need frequent updates
Common Issues and Solutions
Here are some common issues you might encounter when building icon toolbars, along with their solutions:
1. Icons rendering at inconsistent sizes
Problem: Different MUI icons sometimes render at slightly different sizes, creating an uneven appearance.
Solution: Use the fontSize
prop consistently and consider using a wrapper to normalize sizes:
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 24, height: 24 }}>
<DeleteIcon fontSize="small" />
</Box>
2. Tooltip delays causing usability issues
Problem: Default tooltip delays can make the UI feel sluggish.
Solution: Customize the tooltip's enterDelay and leaveDelay:
<Tooltip
title="Delete file"
enterDelay={500}
leaveDelay={200}
arrow
>
<IconButton>
<DeleteIcon />
</IconButton>
</Tooltip>
3. Disabled buttons not showing tooltips
Problem: When an IconButton is disabled, its tooltip doesn't show by default.
Solution: Wrap the IconButton in a span:
<Tooltip title="This action is disabled">
<span>
<IconButton disabled>
<DeleteIcon />
</IconButton>
</span>
</Tooltip>
4. Toolbar overflowing on small screens
Problem: The toolbar extends beyond the screen on mobile devices.
Solution: Implement the responsive pattern with an overflow menu as shown earlier, or use a scrollable container:
<Box
sx={{
overflowX: 'auto',
whiteSpace: 'nowrap',
WebkitOverflowScrolling: 'touch', // For smoother scrolling on iOS
'&::-webkit-scrollbar': { height: 6 },
'&::-webkit-scrollbar-thumb': { backgroundColor: 'rgba(0,0,0,0.2)', borderRadius: 3 }
}}
>
<FileToolbar actions={actions} />
</Box>
5. Inconsistent active state styling
Problem: Toggle buttons don't maintain a consistent active state appearance.
Solution: Create a custom styled component for toggle buttons:
import { styled } from '@mui/material/styles';
import IconButton from '@mui/material/IconButton';
const ToggleIconButton = styled(IconButton, {
shouldForwardProp: (prop) => prop !== 'active',
})(({ theme, active }) => ({
...(active && {
backgroundColor: theme.palette.action.selected,
'&:hover': {
backgroundColor: theme.palette.action.selected,
},
}),
}));
// Usage
<ToggleIconButton active={isActive} onClick={handleToggle}>
<FormatBoldIcon />
</ToggleIconButton>
Wrapping Up
Building an action toolbar with MUI icons is a powerful way to create intuitive interfaces for your React applications. By following the patterns and practices outlined in this guide, you can create toolbars that are accessible, responsive, and visually appealing.
Remember to focus on the user experience by providing clear visual feedback, consistent styling, and proper accessibility attributes. Group related actions together, use tooltips effectively, and ensure your toolbar works well across different screen sizes.
The component-based approach we've developed allows for maximum flexibility while maintaining a clean, reusable codebase. You can easily adapt the patterns shown here to create toolbars for any application, from document editors to file managers and beyond.