Menu

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:

PropTypeDefaultDescription
colorstring'default'The color of the component. Can be 'primary', 'secondary', 'error', 'info', 'success', 'warning', or any custom color defined in your theme.
disabledbooleanfalseIf true, the button will be disabled.
disableFocusRipplebooleanfalseIf true, the keyboard focus ripple will be disabled.
disableRipplebooleanfalseIf true, the ripple effect will be disabled.
edgestringfalseIf given, the button will use additional styles for edge positions. Options are 'start', 'end', or false.
sizestring'medium'The size of the button. Can be 'small', 'medium', or 'large'.
sxobjectThe system prop that allows defining system overrides as well as additional CSS styles.

Customization Options

IconButton can be customized in several ways:

  1. Through props: Basic styling like color and size can be controlled via props.
  2. Using the sx prop: For more specific styling needs, the sx prop provides access to the theme and accepts CSS properties.
  3. Theme customization: You can override the default styles in your theme.
  4. 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:

  1. Always provide an aria-label for buttons without text
  2. Use the Tooltip component to provide additional context
  3. Ensure proper focus states and keyboard navigation
  4. 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:

  1. We wrap the IconButton with a Tooltip to provide context to users
  2. We use a span wrapper around the IconButton to ensure the tooltip works even when the button is disabled
  3. We pass through essential props like color, size, and onClick
  4. We set an aria-label for accessibility
  5. 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:

  1. Uses a Paper component as a container for the toolbar
  2. Implements a Stack component to arrange the buttons either horizontally or vertically
  3. Supports optional dividers between buttons
  4. Allows for global styling and individual button customization
  5. 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:

  1. A complete document editor toolbar with formatting, alignment, and document actions
  2. Toggle buttons that maintain state (bold, italic, underline, alignment)
  3. Different toolbar layouts (horizontal, vertical, grouped)
  4. Custom styling options
  5. 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:

  1. What actions will it contain?
  2. How should it be laid out (horizontal, vertical, grouped)?
  3. Do you need state management for toggle buttons?
  4. 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:

  1. Added IconButton components for each action
  2. Wrapped each button in a Tooltip for accessibility and user guidance
  3. Set appropriate aria-labels for screen readers
  4. Defined click handlers for each action
  5. 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:

  1. We use MUI's useMediaQuery hook to detect screen size
  2. On smaller screens, we show only the first two actions directly
  3. The remaining actions are placed in an overflow menu accessed via a "more" icon
  4. 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:

  1. Proper keyboard navigation using arrow keys
  2. Focus management with tabIndex
  3. Appropriate ARIA roles and labels
  4. 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:

  1. Using state to track toggle button status
  2. Visually indicating active state with background color and text color
  3. 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:

  1. Created separate toolbars for different categories of actions
  2. Used different border colors to visually distinguish the groups
  3. Created a combined toolbar with dividers between sections
  4. 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

  1. Always provide tooltips: Every icon button should have a descriptive tooltip
  2. Use aria-labels: Icon-only buttons need proper aria-labels for screen readers
  3. Ensure keyboard navigation: Users should be able to navigate the toolbar using only the keyboard
  4. Test with screen readers: Verify that your toolbar works with popular screen readers

Visual Design

  1. Maintain consistent spacing: Use consistent margins between buttons
  2. Use color purposefully: Reserve accent colors for important actions or active states
  3. Provide visual feedback: Buttons should have hover, focus, and active states
  4. Group related actions: Use dividers or separate toolbars for different categories of actions

Responsiveness

  1. Adapt to screen size: Show fewer buttons on small screens
  2. Use overflow menus: Move less important actions to a dropdown menu on mobile
  3. Consider touch targets: Ensure buttons are large enough for touch on mobile devices
  4. Test on real devices: Verify that your toolbar works well on various screen sizes

Performance

  1. Memoize event handlers: Use useCallback for event handlers to prevent unnecessary re-renders
  2. Lazy load icons: If you're using many icons, consider loading them dynamically
  3. Minimize state updates: Batch state updates when possible to reduce renders
  4. 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.