Menu

Building Quick Action Menus with MUI Floating Action Button for Mobile Layouts

As a front-end developer working with React applications, creating intuitive mobile interfaces often presents unique challenges. One of the most effective UI patterns for mobile layouts is the Floating Action Button (FAB) combined with a quick action menu. I've implemented this pattern countless times across various projects, and it's become an essential tool in my mobile UI toolkit.

In this article, I'll guide you through creating a responsive, accessible, and visually appealing quick action menu using Material UI's Floating Action Button component. You'll learn not just the basics, but also advanced techniques for customization, animation, and proper integration within your React applications.

Learning Objectives

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

  1. Implement a basic Floating Action Button with Material UI
  2. Create an expandable quick action menu for mobile interfaces
  3. Add smooth animations and transitions to your FAB menu
  4. Customize the appearance and behavior of your FAB components
  5. Handle accessibility concerns for better user experience
  6. Implement advanced patterns like speed dial and conditional rendering
  7. Optimize your FAB menu for performance across devices

Understanding the Floating Action Button Component

The Floating Action Button (FAB) is a circular button that represents the primary action in an application. It's designed to float above the UI, typically in the bottom right corner, making it easily accessible for thumb navigation on mobile devices.

Core FAB Component and Its Properties

Material UI's FAB component is built on top of the ButtonBase component and comes with several variants and customization options. The component is imported from the @mui/material package.

import { Fab } from '@mui/material';

The FAB component accepts numerous props that allow you to customize its appearance and behavior. Here's a comprehensive table of the most important props:

PropTypeDefaultDescription
colorstring'default'The color of the component. Options include 'primary', 'secondary', 'error', 'info', 'success', 'warning', or 'inherit'.
disabledbooleanfalseIf true, the button will be disabled.
disableFocusRipplebooleanfalseIf true, the keyboard focus ripple will be disabled.
disableRipplebooleanfalseIf true, the ripple effect will be disabled.
sizestring'large'The size of the component. Options include 'small', 'medium', and 'large'.
variantstring'circular'The variant to use. Options include 'circular' and 'extended'.
sxobjectThe system prop that allows defining system overrides as well as additional CSS styles.

FAB Variants and Sizes

Material UI's FAB component comes in different variants and sizes to accommodate various design needs. Let's explore these options:

Variants

  1. Circular: The default variant, which renders a circular button.
  2. Extended: A pill-shaped button that can include both an icon and text.
// Circular FAB (default)
<Fab color="primary" aria-label="add">
  <AddIcon />
</Fab>

// Extended FAB
<Fab variant="extended" color="primary" aria-label="add">
  <AddIcon sx={{ mr: 1 }} />
  Create
</Fab>

Sizes

The FAB component offers three different sizes:

  1. Small: Compact size, suitable for less prominent actions
  2. Medium: Standard size
  3. Large: Default size, more prominent and easier to tap on mobile
// Small FAB
<Fab size="small" color="secondary" aria-label="edit">
  <EditIcon />
</Fab>

// Medium FAB
<Fab size="medium" color="secondary" aria-label="edit">
  <EditIcon />
</Fab>

// Large FAB (default)
<Fab color="secondary" aria-label="edit">
  <EditIcon />
</Fab>

Styling and Customization

The FAB component can be styled in multiple ways, including:

  1. Using the sx prop: For inline styling with access to the theme
  2. Theme customization: For global styling of all FAB components
  3. Styled Components API: For creating reusable styled versions

Here's an example of styling a FAB using the sx prop:

<Fab
  color="primary"
  aria-label="add"
  sx={{
    position: 'fixed',
    bottom: 16,
    right: 16,
    boxShadow: 3,
    '&:hover': {
      backgroundColor: 'secondary.main',
    }
  }}
>
  <AddIcon />
</Fab>

For theme customization, you can override the default styles in your theme:

import { createTheme } from '@mui/material/styles';

const theme = createTheme({
  components: {
    MuiFab: {
      styleOverrides: {
        root: {
          boxShadow: '0px 4px 10px rgba(0, 0, 0, 0.25)',
          '&:hover': {
            boxShadow: '0px 6px 15px rgba(0, 0, 0, 0.3)',
          },
        },
        primary: {
          backgroundColor: '#1976d2',
          '&:hover': {
            backgroundColor: '#115293',
          },
        },
      },
    },
  },
});

Accessibility Considerations

When implementing FABs, accessibility should be a primary concern. Here are some best practices:

  1. Always include an aria-label: Since FABs often only contain icons, providing an accessible label is crucial.
  2. Ensure sufficient color contrast: The FAB should stand out against its background.
  3. Provide keyboard navigation: FABs should be focusable and operable via keyboard.
  4. Consider touch target size: FABs should be large enough for easy tapping on mobile (at least 48x48px).
// Accessible FAB implementation
<Fab
  color="primary"
  aria-label="add new item"
  size="large"
  sx={{ minHeight: 56, minWidth: 56 }}
>
  <AddIcon />
</Fab>

Creating a Basic FAB Implementation

Let's start by creating a simple FAB component that we'll later expand into a full quick action menu. This basic implementation will serve as the foundation for our more advanced features.

Setting Up the Project

First, let's set up a new React project and install the necessary dependencies:

npx create-react-app mui-fab-menu
cd mui-fab-menu
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled

Creating a Basic FAB Component

Now, let's create a basic FAB component that we'll place in the bottom right corner of our application:

import React from 'react';
import { Fab } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';

function BasicFab() {
  return (
    <Fab
      color="primary"
      aria-label="add"
      sx={{
        position: 'fixed',
        bottom: 16,
        right: 16,
      }}
    >
      <AddIcon />
    </Fab>
  );
}

export default BasicFab;

In this implementation, we're using the fixed position to ensure the FAB stays in the same position regardless of scrolling. The bottom and right properties position it in the bottom right corner with a 16px margin.

Integrating the FAB into Your App

Let's integrate our BasicFab component into the main App component:

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

// Create a theme instance
const theme = createTheme({
  palette: {
    primary: {
      main: '#1976d2',
    },
    secondary: {
      main: '#dc004e',
    },
  },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <div className="App">
        <header className="App-header">
          <h1>FAB Quick Action Menu Demo</h1>
        </header>
        <main style={{ height: '100vh' }}>
          {/* Your main content here */}
        </main>
        <BasicFab />
      </div>
    </ThemeProvider>
  );
}

export default App;

This gives us a basic FAB that appears in the bottom right corner of the screen. Now, let's enhance it to create a quick action menu.

Building a FAB Quick Action Menu

A quick action menu allows users to access frequently used actions without navigating through multiple screens. Let's build a menu that expands when the main FAB is clicked, revealing additional action buttons.

Creating the Expandable FAB Menu

We'll implement an expandable menu that shows additional FABs when the main FAB is clicked:

import React, { useState } from 'react';
import { Fab, Box, Zoom } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FavoriteIcon from '@mui/icons-material/Favorite';

function ExpandableFabMenu() {
  const [open, setOpen] = useState(false);
  
  const handleToggle = () => {
    setOpen(!open);
  };
  
  // Action items with their respective icons and labels
  const actions = [
    { icon: <EditIcon />, name: 'Edit', color: 'primary' },
    { icon: <DeleteIcon />, name: 'Delete', color: 'error' },
    { icon: <FavoriteIcon />, name: 'Favorite', color: 'secondary' },
  ];
  
  return (
    <Box sx={{ position: 'fixed', bottom: 16, right: 16 }}>
      {/* Render the action buttons with a staggered animation when open */}
      {open && (
        <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 2 }}>
          {actions.map((action, index) => (
            <Zoom
              key={action.name}
              in={open}
              style={{
                transitionDelay: open ? `${index * 100}ms` : '0ms',
              }}
            >
              <Fab
                size="small"
                color={action.color}
                aria-label={action.name}
              >
                {action.icon}
              </Fab>
            </Zoom>
          ))}
        </Box>
      )}
      
      {/* Main FAB button that toggles the menu */}
      <Fab
        color="primary"
        aria-label={open ? 'close menu' : 'open menu'}
        onClick={handleToggle}
        sx={{
          transform: open ? 'rotate(45deg)' : 'rotate(0deg)',
          transition: 'transform 0.3s ease-in-out',
        }}
      >
        <AddIcon />
      </Fab>
    </Box>
  );
}

export default ExpandableFabMenu;

Let's break down this implementation:

  1. We use a useState hook to track whether the menu is open or closed.
  2. The handleToggle function toggles the menu state when the main FAB is clicked.
  3. We define an array of action items, each with an icon, name, and color.
  4. When the menu is open, we render each action button inside a Zoom component for a staggered animation effect.
  5. The main FAB rotates 45 degrees when the menu is open, turning the plus icon into an X to indicate that clicking it will close the menu.

Adding Transitions and Animations

To make our FAB menu more visually appealing, we've used the Zoom component from Material UI for animations. We've also added a rotation transition to the main FAB.

The transitionDelay property ensures that the action buttons appear in sequence, creating a staggered animation effect. Each button appears 100ms after the previous one, making the expansion feel more natural.

Handling Backdrop and Click Outside

To improve the user experience, let's add a backdrop that appears when the menu is open. Clicking anywhere on the backdrop will close the menu:

import React, { useState } from 'react';
import { Fab, Box, Zoom, Backdrop } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FavoriteIcon from '@mui/icons-material/Favorite';

function ExpandableFabMenu() {
  const [open, setOpen] = useState(false);
  
  const handleToggle = () => {
    setOpen(!open);
  };
  
  const handleClose = () => {
    setOpen(false);
  };
  
  // Action items with their respective icons and labels
  const actions = [
    { icon: <EditIcon />, name: 'Edit', color: 'primary' },
    { icon: <DeleteIcon />, name: 'Delete', color: 'error' },
    { icon: <FavoriteIcon />, name: 'Favorite', color: 'secondary' },
  ];
  
  return (
    <>
      <Backdrop
        open={open}
        onClick={handleClose}
        sx={{ zIndex: 1200, backgroundColor: 'rgba(0, 0, 0, 0.5)' }}
      />
      
      <Box sx={{ position: 'fixed', bottom: 16, right: 16, zIndex: 1300 }}>
        {/* Render the action buttons with a staggered animation when open */}
        {open && (
          <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 2 }}>
            {actions.map((action, index) => (
              <Zoom
                key={action.name}
                in={open}
                style={{
                  transitionDelay: open ? `${index * 100}ms` : '0ms',
                }}
              >
                <Fab
                  size="small"
                  color={action.color}
                  aria-label={action.name}
                >
                  {action.icon}
                </Fab>
              </Zoom>
            ))}
          </Box>
        )}
        
        {/* Main FAB button that toggles the menu */}
        <Fab
          color="primary"
          aria-label={open ? 'close menu' : 'open menu'}
          onClick={handleToggle}
          sx={{
            transform: open ? 'rotate(45deg)' : 'rotate(0deg)',
            transition: 'transform 0.3s ease-in-out',
          }}
        >
          <AddIcon />
        </Fab>
      </Box>
    </>
  );
}

export default ExpandableFabMenu;

The Backdrop component creates a semi-transparent overlay that covers the entire screen when the menu is open. Clicking on this backdrop triggers the handleClose function, which closes the menu.

We've also added appropriate zIndex values to ensure that the menu appears above the backdrop, and the backdrop appears above the rest of the content.

Implementing a Speed Dial Component

Material UI provides a specialized component called SpeedDial that's specifically designed for creating FAB menus. Let's implement our quick action menu using this component:

Basic Speed Dial Implementation

import React, { useState } from 'react';
import { SpeedDial, SpeedDialIcon, SpeedDialAction } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';

function BasicSpeedDial() {
  const [open, setOpen] = useState(false);
  
  const handleOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);
  
  const actions = [
    { icon: <EditIcon />, name: 'Edit' },
    { icon: <DeleteIcon />, name: 'Delete' },
    { icon: <FavoriteIcon />, name: 'Favorite' },
    { icon: <ShareIcon />, name: 'Share' },
  ];
  
  return (
    <SpeedDial
      ariaLabel="Quick actions"
      sx={{ position: 'fixed', bottom: 16, right: 16 }}
      icon={<SpeedDialIcon />}
      onClose={handleClose}
      onOpen={handleOpen}
      open={open}
    >
      {actions.map((action) => (
        <SpeedDialAction
          key={action.name}
          icon={action.icon}
          tooltipTitle={action.name}
          tooltipOpen
          onClick={handleClose}
        />
      ))}
    </SpeedDial>
  );
}

export default BasicSpeedDial;

The SpeedDial component handles much of the functionality we implemented manually earlier, including:

  1. Opening and closing the menu
  2. Animating the action buttons
  3. Transforming the main FAB icon

Speed Dial Directions

The SpeedDial component supports different directions for expanding the menu, which is particularly useful for different screen layouts:

import React, { useState } from 'react';
import { SpeedDial, SpeedDialIcon, SpeedDialAction, FormControl, InputLabel, Select, MenuItem, Box } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';

function DirectionalSpeedDial() {
  const [open, setOpen] = useState(false);
  const [direction, setDirection] = useState('up');
  
  const handleDirectionChange = (event) => {
    setDirection(event.target.value);
  };
  
  const handleOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);
  
  const actions = [
    { icon: <EditIcon />, name: 'Edit' },
    { icon: <DeleteIcon />, name: 'Delete' },
    { icon: <FavoriteIcon />, name: 'Favorite' },
    { icon: <ShareIcon />, name: 'Share' },
  ];
  
  // Position the SpeedDial based on the selected direction
  const getSpeedDialPosition = () => {
    switch (direction) {
      case 'up':
        return { bottom: 16, right: 16 };
      case 'down':
        return { top: 16, right: 16 };
      case 'left':
        return { bottom: 16, right: 16 };
      case 'right':
        return { bottom: 16, left: 16 };
      default:
        return { bottom: 16, right: 16 };
    }
  };
  
  return (
    <>
      <Box sx={{ m: 2 }}>
        <FormControl>
          <InputLabel>Direction</InputLabel>
          <Select
            value={direction}
            onChange={handleDirectionChange}
            label="Direction"
          >
            <MenuItem value="up">Up</MenuItem>
            <MenuItem value="down">Down</MenuItem>
            <MenuItem value="left">Left</MenuItem>
            <MenuItem value="right">Right</MenuItem>
          </Select>
        </FormControl>
      </Box>
      
      <SpeedDial
        ariaLabel="Quick actions"
        sx={{ position: 'fixed', ...getSpeedDialPosition() }}
        icon={<SpeedDialIcon />}
        onClose={handleClose}
        onOpen={handleOpen}
        open={open}
        direction={direction}
      >
        {actions.map((action) => (
          <SpeedDialAction
            key={action.name}
            icon={action.icon}
            tooltipTitle={action.name}
            tooltipOpen
            onClick={handleClose}
          />
        ))}
      </SpeedDial>
    </>
  );
}

export default DirectionalSpeedDial;

In this example, we've added a dropdown to select the direction of the menu expansion. The direction prop of the SpeedDial component determines which way the actions will appear when the menu opens.

Speed Dial Props and Customization

The SpeedDial component and its related components (SpeedDialIcon and SpeedDialAction) offer numerous props for customization:

ComponentPropTypeDescription
SpeedDialariaLabelstringRequired. The aria-label of the SpeedDial for accessibility.
SpeedDialdirectionstringThe direction the actions open. Options: 'up', 'down', 'left', 'right'.
SpeedDialhiddenbooleanIf true, the SpeedDial will be hidden.
SpeedDialiconnodeThe icon to display in the SpeedDial. Usually a SpeedDialIcon component.
SpeedDialonClosefunctionCallback fired when the component requests to be closed.
SpeedDialonOpenfunctionCallback fired when the component requests to be open.
SpeedDialopenbooleanIf true, the component is shown.
SpeedDialopenIconnodeThe icon to display in the SpeedDial when it's open.
SpeedDialFabPropsobjectProps applied to the Fab component.
SpeedDialActioniconnodeThe icon to display in the SpeedDialAction.
SpeedDialActiontooltipTitlenodeThe text to display in the tooltip.
SpeedDialActiontooltipOpenbooleanIf true, the tooltip is always open.
SpeedDialActionFabPropsobjectProps applied to the Fab component.
SpeedDialIconiconnodeThe icon to display in the closed state.
SpeedDialIconopenIconnodeThe icon to display in the open state.

Let's customize our SpeedDial component with some of these props:

import React, { useState } from 'react';
import { SpeedDial, SpeedDialIcon, SpeedDialAction } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';
import AddIcon from '@mui/icons-material/Add';
import CloseIcon from '@mui/icons-material/Close';

function CustomizedSpeedDial() {
  const [open, setOpen] = useState(false);
  
  const handleOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);
  
  const actions = [
    { icon: <EditIcon />, name: 'Edit' },
    { icon: <DeleteIcon />, name: 'Delete' },
    { icon: <FavoriteIcon />, name: 'Favorite' },
    { icon: <ShareIcon />, name: 'Share' },
  ];
  
  return (
    <SpeedDial
      ariaLabel="Quick actions"
      sx={{
        position: 'fixed',
        bottom: 16,
        right: 16,
      }}
      icon={<SpeedDialIcon icon={<AddIcon />} openIcon={<CloseIcon />} />}
      onClose={handleClose}
      onOpen={handleOpen}
      open={open}
      FabProps={{
        color: 'secondary',
        size: 'large',
        sx: {
          boxShadow: '0px 4px 10px rgba(0, 0, 0, 0.25)',
          '&:hover': {
            boxShadow: '0px 6px 15px rgba(0, 0, 0, 0.3)',
          },
        },
      }}
    >
      {actions.map((action) => (
        <SpeedDialAction
          key={action.name}
          icon={action.icon}
          tooltipTitle={action.name}
          tooltipOpen
          onClick={handleClose}
          FabProps={{
            color: 'primary',
            size: 'small',
            sx: {
              boxShadow: '0px 2px 5px rgba(0, 0, 0, 0.2)',
            },
          }}
        />
      ))}
    </SpeedDial>
  );
}

export default CustomizedSpeedDial;

In this example, we've customized:

  1. The main FAB with custom icons for both open and closed states
  2. The appearance of the main FAB using the FabProps prop
  3. The appearance of the action FABs using the FabProps prop on each SpeedDialAction

Creating a Context-Aware FAB Menu

Now, let's build a more sophisticated FAB menu that changes its actions based on the current context of the application. This is particularly useful for mobile applications where screen real estate is limited.

Implementing Context-Aware Actions

import React, { useState, useContext, createContext } from 'react';
import { SpeedDial, SpeedDialIcon, SpeedDialAction } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';
import FilterListIcon from '@mui/icons-material/FilterList';
import SortIcon from '@mui/icons-material/Sort';
import SearchIcon from '@mui/icons-material/Search';

// Create a context to track the current page/view
const PageContext = createContext('home');

// Actions for different pages
const pageActions = {
  home: [
    { icon: <AddIcon />, name: 'Create New', action: () => console.log('Create New') },
    { icon: <FilterListIcon />, name: 'Filter', action: () => console.log('Filter') },
    { icon: <SortIcon />, name: 'Sort', action: () => console.log('Sort') },
    { icon: <SearchIcon />, name: 'Search', action: () => console.log('Search') },
  ],
  detail: [
    { icon: <EditIcon />, name: 'Edit', action: () => console.log('Edit') },
    { icon: <DeleteIcon />, name: 'Delete', action: () => console.log('Delete') },
    { icon: <FavoriteIcon />, name: 'Favorite', action: () => console.log('Favorite') },
    { icon: <ShareIcon />, name: 'Share', action: () => console.log('Share') },
  ],
};

function ContextAwareFab() {
  const [open, setOpen] = useState(false);
  const currentPage = useContext(PageContext);
  
  const handleOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);
  
  // Get actions for the current page
  const actions = pageActions[currentPage] || pageActions.home;
  
  const handleActionClick = (action) => {
    action();
    handleClose();
  };
  
  return (
    <SpeedDial
      ariaLabel="Context actions"
      sx={{ position: 'fixed', bottom: 16, right: 16 }}
      icon={<SpeedDialIcon />}
      onClose={handleClose}
      onOpen={handleOpen}
      open={open}
    >
      {actions.map((action) => (
        <SpeedDialAction
          key={action.name}
          icon={action.icon}
          tooltipTitle={action.name}
          tooltipOpen
          onClick={() => handleActionClick(action.action)}
        />
      ))}
    </SpeedDial>
  );
}

// A demo component that allows switching between pages
function ContextAwareFabDemo() {
  const [currentPage, setCurrentPage] = useState('home');
  
  return (
    <PageContext.Provider value={currentPage}>
      <div style={{ padding: 16 }}>
        <h2>Current Page: {currentPage}</h2>
        <button onClick={() => setCurrentPage('home')}>
          Go to Home Page
        </button>
        <button onClick={() => setCurrentPage('detail')}
          style={{ marginLeft: 8 }}
        >
          Go to Detail Page
        </button>
        <ContextAwareFab />
      </div>
    </PageContext.Provider>
  );
}

export default ContextAwareFabDemo;

In this implementation:

  1. We create a context to track the current page or view.
  2. We define different sets of actions for different pages.
  3. The FAB menu dynamically changes its actions based on the current page.
  4. Each action has an associated function that gets called when the action button is clicked.

This approach allows the FAB menu to adapt to the user's current context, providing the most relevant actions for each screen.

Advanced Patterns and Best Practices

Let's explore some advanced patterns and best practices for implementing FAB menus in mobile layouts.

Conditional Rendering Based on Screen Size

For responsive designs, you might want to show the FAB menu only on mobile screens and use different UI patterns on larger screens:

import React, { useState, useEffect } from 'react';
import { SpeedDial, SpeedDialIcon, SpeedDialAction, useMediaQuery, useTheme } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';

function ResponsiveFabMenu() {
  const [open, setOpen] = useState(false);
  const theme = useTheme();
  const isMobile = useMediaQuery(theme.breakpoints.down('md'));
  
  const handleOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);
  
  const actions = [
    { icon: <EditIcon />, name: 'Edit' },
    { icon: <DeleteIcon />, name: 'Delete' },
    { icon: <FavoriteIcon />, name: 'Favorite' },
    { icon: <ShareIcon />, name: 'Share' },
  ];
  
  // If not on mobile, don't render the FAB menu
  if (!isMobile) {
    return null;
  }
  
  return (
    <SpeedDial
      ariaLabel="Quick actions"
      sx={{ position: 'fixed', bottom: 16, right: 16 }}
      icon={<SpeedDialIcon />}
      onClose={handleClose}
      onOpen={handleOpen}
      open={open}
    >
      {actions.map((action) => (
        <SpeedDialAction
          key={action.name}
          icon={action.icon}
          tooltipTitle={action.name}
          tooltipOpen
          onClick={handleClose}
        />
      ))}
    </SpeedDial>
  );
}

export default ResponsiveFabMenu;

This component uses Material UI's useMediaQuery hook to check if the current screen size is below the "medium" breakpoint. If it's not a mobile device, the component returns null, effectively not rendering the FAB menu.

Scroll-Aware FAB

Another useful pattern is to hide or show the FAB based on the scroll direction. This keeps the FAB out of the way when users are scrolling down to read content, but makes it available when they're scrolling up:

import React, { useState, useEffect } from 'react';
import { Fab, Zoom } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';

function ScrollAwareFab() {
  const [visible, setVisible] = useState(true);
  const [lastScrollY, setLastScrollY] = useState(0);
  
  useEffect(() => {
    const handleScroll = () => {
      const currentScrollY = window.scrollY;
      
      // Show FAB when scrolling up, hide when scrolling down
      if (currentScrollY < lastScrollY || currentScrollY < 100) {
        setVisible(true);
      } else {
        setVisible(false);
      }
      
      setLastScrollY(currentScrollY);
    };
    
    window.addEventListener('scroll', handleScroll, { passive: true });
    
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, [lastScrollY]);
  
  return (
    <Zoom in={visible}>
      <Fab
        color="primary"
        aria-label="add"
        sx={{
          position: 'fixed',
          bottom: 16,
          right: 16,
          transition: 'transform 0.3s ease-in-out',
        }}
      >
        <AddIcon />
      </Fab>
    </Zoom>
  );
}

export default ScrollAwareFab;

This component:

  1. Tracks the scroll position using the useEffect hook.
  2. Shows the FAB when the user is scrolling up or near the top of the page.
  3. Hides the FAB when the user is scrolling down.
  4. Uses Material UI's Zoom component for a smooth transition effect.

Performance Optimization

When implementing FAB menus, especially with animations and context-awareness, performance can become a concern. Here are some optimization techniques:

import React, { useState, useCallback, memo } from 'react';
import { SpeedDial, SpeedDialIcon, SpeedDialAction } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';

// Memoized action component to prevent unnecessary re-renders
const MemoizedSpeedDialAction = memo(function Action({ icon, name, onClick }) {
  return (
    <SpeedDialAction
      icon={icon}
      tooltipTitle={name}
      tooltipOpen
      onClick={onClick}
    />
  );
});

function OptimizedFabMenu() {
  const [open, setOpen] = useState(false);
  
  // Memoized handlers to prevent recreating functions on each render
  const handleOpen = useCallback(() => setOpen(true), []);
  const handleClose = useCallback(() => setOpen(false), []);
  
  // Pre-defined actions array to prevent recreation on each render
  const actions = [
    { icon: <EditIcon />, name: 'Edit', action: () => console.log('Edit') },
    { icon: <DeleteIcon />, name: 'Delete', action: () => console.log('Delete') },
    { icon: <FavoriteIcon />, name: 'Favorite', action: () => console.log('Favorite') },
    { icon: <ShareIcon />, name: 'Share', action: () => console.log('Share') },
  ];
  
  // Memoized action click handler
  const handleActionClick = useCallback((action) => {
    action();
    handleClose();
  }, [handleClose]);
  
  return (
    <SpeedDial
      ariaLabel="Quick actions"
      sx={{ position: 'fixed', bottom: 16, right: 16 }}
      icon={<SpeedDialIcon />}
      onClose={handleClose}
      onOpen={handleOpen}
      open={open}
    >
      {actions.map((action) => (
        <MemoizedSpeedDialAction
          key={action.name}
          icon={action.icon}
          name={action.name}
          onClick={() => handleActionClick(action.action)}
        />
      ))}
    </SpeedDial>
  );
}

export default OptimizedFabMenu;

This optimized implementation:

  1. Uses memo to prevent unnecessary re-renders of the SpeedDialAction components.
  2. Uses useCallback to memoize event handlers, preventing new function instances on each render.
  3. Pre-defines the actions array outside of the render cycle to prevent recreation on each render.

These optimizations are particularly important for complex FAB menus with many actions or when the parent component renders frequently.

Integrating with Navigation and Forms

FAB menus often need to interact with navigation or form functionality. Let's explore some common integration patterns.

FAB for Form Submission

import React, { useState } from 'react';
import { Fab, TextField, Box, Snackbar } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';

function FormWithFab() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  const [snackbarOpen, setSnackbarOpen] = useState(false);
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Form submitted:', formData);
    setSnackbarOpen(true);
  };
  
  const handleCloseSnackbar = () => {
    setSnackbarOpen(false);
  };
  
  return (
    <Box sx={{ padding: 2, position: 'relative', minHeight: '100vh' }}>
      <form onSubmit={handleSubmit}>
        <Box sx={{ mb: 2 }}>
          <TextField
            fullWidth
            label="Name"
            name="name"
            value={formData.name}
            onChange={handleChange}
            required
          />
        </Box>
        <Box sx={{ mb: 2 }}>
          <TextField
            fullWidth
            label="Email"
            name="email"
            type="email"
            value={formData.email}
            onChange={handleChange}
            required
          />
        </Box>
        
        {/* Hidden submit button for form submission */}
        <button type="submit" style={{ display: 'none' }} />
      </form>
      
      {/* FAB that triggers form submission */}
      <Fab
        color="primary"
        aria-label="save"
        sx={{ position: 'fixed', bottom: 16, right: 16 }}
        onClick={() => document.querySelector('form').requestSubmit()}
      >
        <SaveIcon />
      </Fab>
      
      <Snackbar
        open={snackbarOpen}
        autoHideDuration={3000}
        onClose={handleCloseSnackbar}
        message="Form submitted successfully!"
      />
    </Box>
  );
}

export default FormWithFab;

In this example:

  1. We create a simple form with name and email fields.
  2. The FAB acts as the submit button for the form.
  3. We use the requestSubmit() method to trigger form submission programmatically.
  4. A Snackbar displays a success message when the form is submitted.

This pattern is particularly useful for mobile forms where the primary action (submit) should be easily accessible without scrolling to the bottom of the form.

FAB for Navigation

import React, { useState } from 'react';
import { SpeedDial, SpeedDialIcon, SpeedDialAction, Box } from '@mui/material';
import HomeIcon from '@mui/icons-material/Home';
import PersonIcon from '@mui/icons-material/Person';
import SettingsIcon from '@mui/icons-material/Settings';
import InfoIcon from '@mui/icons-material/Info';

function NavigationFab() {
  const [open, setOpen] = useState(false);
  const [currentPage, setCurrentPage] = useState('home');
  
  const handleOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);
  
  const pages = [
    { icon: <HomeIcon />, name: 'Home', id: 'home' },
    { icon: <PersonIcon />, name: 'Profile', id: 'profile' },
    { icon: <SettingsIcon />, name: 'Settings', id: 'settings' },
    { icon: <InfoIcon />, name: 'About', id: 'about' },
  ];
  
  const navigateTo = (pageId) => {
    setCurrentPage(pageId);
    handleClose();
  };
  
  return (
    <Box sx={{ padding: 2, minHeight: '100vh' }}>
      <h1>{pages.find(page => page.id === currentPage)?.name || 'Page'}</h1>
      <p>This is the {currentPage} page content.</p>
      
      <SpeedDial
        ariaLabel="Navigation"
        sx={{ position: 'fixed', bottom: 16, right: 16 }}
        icon={<SpeedDialIcon />}
        onClose={handleClose}
        onOpen={handleOpen}
        open={open}
      >
        {pages.map((page) => (
          <SpeedDialAction
            key={page.id}
            icon={page.icon}
            tooltipTitle={page.name}
            tooltipOpen
            onClick={() => navigateTo(page.id)}
          />
        ))}
      </SpeedDial>
    </Box>
  );
}

export default NavigationFab;

This component:

  1. Uses a FAB menu for navigation between different pages or views.
  2. Each action in the menu represents a different page.
  3. Clicking on an action changes the current page and updates the UI accordingly.

This pattern is useful for single-page applications or mobile apps where navigation should be easily accessible from any point in the application.

Accessibility Enhancements

Accessibility is crucial for ensuring that all users can interact with your FAB menu. Let's implement some accessibility enhancements:

import React, { useState, useRef } from 'react';
import { Fab, Box, Zoom, ClickAwayListener, Tooltip } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FavoriteIcon from '@mui/icons-material/Favorite';

function AccessibleFabMenu() {
  const [open, setOpen] = useState(false);
  const mainFabRef = useRef(null);
  const actionRefs = useRef([]);
  
  const handleToggle = () => {
    setOpen(!open);
  };
  
  const handleClose = () => {
    setOpen(false);
    // Return focus to the main FAB when the menu is closed
    mainFabRef.current?.focus();
  };
  
  const handleKeyDown = (e, index) => {
    // Handle keyboard navigation within the menu
    if (open) {
      const actionCount = actions.length;
      
      switch (e.key) {
        case 'Escape':
          handleClose();
          break;
        case 'ArrowUp':
          e.preventDefault();
          const prevIndex = (index - 1 + actionCount) % actionCount;
          actionRefs.current[prevIndex]?.focus();
          break;
        case 'ArrowDown':
          e.preventDefault();
          const nextIndex = (index + 1) % actionCount;
          actionRefs.current[nextIndex]?.focus();
          break;
        default:
          break;
      }
    }
  };
  
  const actions = [
    { icon: <EditIcon />, name: 'Edit', color: 'primary' },
    { icon: <DeleteIcon />, name: 'Delete', color: 'error' },
    { icon: <FavoriteIcon />, name: 'Favorite', color: 'secondary' },
  ];
  
  return (
    <ClickAwayListener onClickAway={handleClose}>
      <Box sx={{ position: 'fixed', bottom: 16, right: 16 }}>
        {/* Action buttons */}
        {open && (
          <Box
            sx={{
              display: 'flex',
              flexDirection: 'column',
              gap: 2,
              mb: 2,
            }}
            role="menu"
            aria-orientation="vertical"
            aria-labelledby="main-fab"
          >
            {actions.map((action, index) => (
              <Zoom
                key={action.name}
                in={open}
                style={{
                  transitionDelay: open ? `${index * 100}ms` : '0ms',
                }}
              >
                <Tooltip title={action.name} placement="left">
                  <Fab
                    size="small"
                    color={action.color}
                    aria-label={action.name}
                    ref={(el) => (actionRefs.current[index] = el)}
                    onKeyDown={(e) => handleKeyDown(e, index)}
                    role="menuitem"
                    tabIndex={open ? 0 : -1}
                  >
                    {action.icon}
                  </Fab>
                </Tooltip>
              </Zoom>
            ))}
          </Box>
        )}
        
        {/* Main FAB button */}
        <Fab
          color="primary"
          aria-label={open ? 'close menu' : 'open menu'}
          aria-haspopup="menu"
          aria-expanded={open}
          aria-controls={open ? 'fab-menu' : undefined}
          id="main-fab"
          onClick={handleToggle}
          ref={mainFabRef}
          sx={{
            transform: open ? 'rotate(45deg)' : 'rotate(0deg)',
            transition: 'transform 0.3s ease-in-out',
          }}
        >
          <AddIcon />
        </Fab>
      </Box>
    </ClickAwayListener>
  );
}

export default AccessibleFabMenu;

This implementation includes several accessibility enhancements:

  1. Proper ARIA attributes: We use role="menu", aria-orientation, aria-labelledby, aria-haspopup, aria-expanded, and aria-controls to provide semantic information to screen readers.
  2. Keyboard navigation: We implement keyboard navigation within the menu using arrow keys.
  3. Focus management: We maintain focus within the menu when it's open and return focus to the main FAB when it closes.
  4. Tooltips: We use tooltips to provide additional information about each action.
  5. Escape key handling: We close the menu when the Escape key is pressed.

These enhancements ensure that users who rely on keyboard navigation or screen readers can effectively use the FAB menu.

Troubleshooting Common Issues

When implementing FAB menus, you might encounter some common issues. Here are solutions to these problems:

Z-Index and Stacking Context

import React, { useState } from 'react';
import { Fab, Box, Modal } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';

function ZIndexDemo() {
  const [modalOpen, setModalOpen] = useState(false);
  
  const handleOpenModal = () => setModalOpen(true);
  const handleCloseModal = () => setModalOpen(false);
  
  return (
    <Box sx={{ padding: 2 }}>
      <button onClick={handleOpenModal}>Open Modal</button>
      
      {/* Modal with a high z-index */}
      <Modal
        open={modalOpen}
        onClose={handleCloseModal}
        aria-labelledby="modal-title"
        aria-describedby="modal-description"
      >
        <Box
          sx={{
            position: 'absolute',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            width: 400,
            bgcolor: 'background.paper',
            boxShadow: 24,
            p: 4,
          }}
        >
          <h2 id="modal-title">Modal Title</h2>
          <p id="modal-description">
            This modal has a high z-index, but the FAB is configured to appear above it.
          </p>
        </Box>
      </Modal>
      
      {/* FAB with an even higher z-index */}
      <Fab
        color="primary"
        aria-label="add"
        sx={{
          position: 'fixed',
          bottom: 16,
          right: 16,
          zIndex: 1500, // Higher than Modal's default z-index (1300)
        }}
      >
        <AddIcon />
      </Fab>
    </Box>
  );
}

export default ZIndexDemo;

In this example, we ensure that the FAB appears above the modal by setting a higher z-index. Material UI's Modal component has a default z-index of 1300, so we set the FAB's z-index to 1500 to make it appear on top.

Touch Target Size

import React from 'react';
import { Fab, Box } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';

function TouchTargetDemo() {
  return (
    <Box sx={{ padding: 2 }}>
      <h2>Touch Target Size Comparison</h2>
      
      <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }}>
        {/* Small FAB - may be difficult to tap on mobile */}
        <Box>
          <p>Small FAB (40px)</p>
          <Fab
            size="small"
            color="primary"
            aria-label="add small"
          >
            <AddIcon />
          </Fab>
        </Box>
        
        {/* Medium FAB - better for touch */}
        <Box>
          <p>Medium FAB (48px)</p>
          <Fab
            size="medium"
            color="primary"
            aria-label="add medium"
          >
            <AddIcon />
          </Fab>
        </Box>
        
        {/* Large FAB - best for primary actions */}
        <Box>
          <p>Large FAB (56px)</p>
          <Fab
            color="primary"
            aria-label="add large"
          >
            <AddIcon />
          </Fab>
        </Box>
        
        {/* Custom sized FAB with padding for larger touch target */}
        <Box>
          <p>Custom FAB with touch padding</p>
          <Fab
            size="small"
            color="primary"
            aria-label="add custom"
            sx={{
              // The visible button is small
              '& .MuiFab-root': {
                width: 40,
                height: 40,
              },
              // But the touch target is larger
              position: 'relative',
              '&::after': {
                content: '""',
                position: 'absolute',
                top: -8,
                right: -8,
                bottom: -8,
                left: -8,
                // Make the touch target invisible but still clickable
                pointerEvents: 'auto',
              },
            }}
          >
            <AddIcon />
          </Fab>
        </Box>
      </Box>
      
      <p>
        For mobile interfaces, it's recommended to use at least medium-sized FABs (48px) 
        to ensure they meet the minimum touch target size guidelines (44-48px).
        For primary actions, use large FABs (56px) for the best user experience.
      </p>
    </Box>
  );
}

export default TouchTargetDemo;

This component demonstrates different FAB sizes and their suitability for touch interfaces. It also shows how to create a custom FAB with a larger touch target area without increasing the visible size of the button.

Animation Performance

import React, { useState } from 'react';
import { Fab, Box, Zoom } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FavoriteIcon from '@mui/icons-material/Favorite';

function PerformanceOptimizedFab() {
  const [open, setOpen] = useState(false);
  
  const handleToggle = () => {
    setOpen(!open);
  };
  
  const actions = [
    { icon: <EditIcon />, name: 'Edit', color: 'primary' },
    { icon: <DeleteIcon />, name: 'Delete', color: 'error' },
    { icon: <FavoriteIcon />, name: 'Favorite', color: 'secondary' },
  ];
  
  return (
    <Box sx={{ position: 'fixed', bottom: 16, right: 16 }}>
      {/* Action buttons with hardware acceleration */}
      {open && (
        <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 2 }}>
          {actions.map((action, index) => (
            <Zoom
              key={action.name}
              in={open}
              style={{
                transitionDelay: open ? `${index * 100}ms` : '0ms',
              }}
            >
              <Fab
                size="small"
                color={action.color}
                aria-label={action.name}
                sx={{
                  // Enable hardware acceleration for smoother animations
                  transform: 'translateZ(0)',
                  willChange: 'transform, opacity',
                }}
              >
                {action.icon}
              </Fab>
            </Zoom>
          ))}
        </Box>
      )}
      
      {/* Main FAB button with hardware acceleration */}
      <Fab
        color="primary"
        aria-label={open ? 'close menu' : 'open menu'}
        onClick={handleToggle}
        sx={{
          transform: open ? 'rotate(45deg) translateZ(0)' : 'rotate(0deg) translateZ(0)',
          transition: 'transform 0.3s ease-in-out',
          willChange: 'transform',
        }}
      >
        <AddIcon />
      </Fab>
    </Box>
  );
}

export default PerformanceOptimizedFab;

This implementation includes performance optimizations for animations:

  1. We use transform: translateZ(0) to enable hardware acceleration.
  2. We use willChange: 'transform, opacity' to hint to the browser that these properties will change, allowing it to optimize in advance.
  3. We keep animations simple and focused on transform and opacity, which are the most performant properties to animate.

These optimizations help ensure smooth animations, especially on mobile devices with limited processing power.

Wrapping Up

In this comprehensive guide, we've explored how to create effective quick action menus using Material UI's Floating Action Button component. We've covered everything from basic implementations to advanced patterns, accessibility enhancements, and performance optimizations.

The FAB is a powerful UI element for mobile layouts, providing quick access to important actions without cluttering the interface. By following the patterns and best practices outlined in this guide, you can create intuitive, accessible, an