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:
- Implement a basic Floating Action Button with Material UI
- Create an expandable quick action menu for mobile interfaces
- Add smooth animations and transitions to your FAB menu
- Customize the appearance and behavior of your FAB components
- Handle accessibility concerns for better user experience
- Implement advanced patterns like speed dial and conditional rendering
- 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:
Prop | Type | Default | Description |
---|---|---|---|
color | string | 'default' | The color of the component. Options include 'primary', 'secondary', 'error', 'info', 'success', 'warning', or 'inherit'. |
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. |
size | string | 'large' | The size of the component. Options include 'small', 'medium', and 'large'. |
variant | string | 'circular' | The variant to use. Options include 'circular' and 'extended'. |
sx | object | The 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
- Circular: The default variant, which renders a circular button.
- 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:
- Small: Compact size, suitable for less prominent actions
- Medium: Standard size
- 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:
- Using the
sx
prop: For inline styling with access to the theme - Theme customization: For global styling of all FAB components
- 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:
- Always include an
aria-label
: Since FABs often only contain icons, providing an accessible label is crucial. - Ensure sufficient color contrast: The FAB should stand out against its background.
- Provide keyboard navigation: FABs should be focusable and operable via keyboard.
- 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:
- We use a
useState
hook to track whether the menu is open or closed. - The
handleToggle
function toggles the menu state when the main FAB is clicked. - We define an array of action items, each with an icon, name, and color.
- When the menu is open, we render each action button inside a
Zoom
component for a staggered animation effect. - 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:
- Opening and closing the menu
- Animating the action buttons
- 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:
Component | Prop | Type | Description |
---|---|---|---|
SpeedDial | ariaLabel | string | Required. The aria-label of the SpeedDial for accessibility. |
SpeedDial | direction | string | The direction the actions open. Options: 'up', 'down', 'left', 'right'. |
SpeedDial | hidden | boolean | If true, the SpeedDial will be hidden. |
SpeedDial | icon | node | The icon to display in the SpeedDial. Usually a SpeedDialIcon component. |
SpeedDial | onClose | function | Callback fired when the component requests to be closed. |
SpeedDial | onOpen | function | Callback fired when the component requests to be open. |
SpeedDial | open | boolean | If true, the component is shown. |
SpeedDial | openIcon | node | The icon to display in the SpeedDial when it's open. |
SpeedDial | FabProps | object | Props applied to the Fab component. |
SpeedDialAction | icon | node | The icon to display in the SpeedDialAction. |
SpeedDialAction | tooltipTitle | node | The text to display in the tooltip. |
SpeedDialAction | tooltipOpen | boolean | If true, the tooltip is always open. |
SpeedDialAction | FabProps | object | Props applied to the Fab component. |
SpeedDialIcon | icon | node | The icon to display in the closed state. |
SpeedDialIcon | openIcon | node | The 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:
- The main FAB with custom icons for both open and closed states
- The appearance of the main FAB using the
FabProps
prop - The appearance of the action FABs using the
FabProps
prop on eachSpeedDialAction
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:
- We create a context to track the current page or view.
- We define different sets of actions for different pages.
- The FAB menu dynamically changes its actions based on the current page.
- 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:
- Tracks the scroll position using the
useEffect
hook. - Shows the FAB when the user is scrolling up or near the top of the page.
- Hides the FAB when the user is scrolling down.
- 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:
- Uses
memo
to prevent unnecessary re-renders of theSpeedDialAction
components. - Uses
useCallback
to memoize event handlers, preventing new function instances on each render. - 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:
- We create a simple form with name and email fields.
- The FAB acts as the submit button for the form.
- We use the
requestSubmit()
method to trigger form submission programmatically. - 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:
- Uses a FAB menu for navigation between different pages or views.
- Each action in the menu represents a different page.
- 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:
- Proper ARIA attributes: We use
role="menu"
,aria-orientation
,aria-labelledby
,aria-haspopup
,aria-expanded
, andaria-controls
to provide semantic information to screen readers. - Keyboard navigation: We implement keyboard navigation within the menu using arrow keys.
- Focus management: We maintain focus within the menu when it's open and return focus to the main FAB when it closes.
- Tooltips: We use tooltips to provide additional information about each action.
- 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:
- We use
transform: translateZ(0)
to enable hardware acceleration. - We use
willChange: 'transform, opacity'
to hint to the browser that these properties will change, allowing it to optimize in advance. - 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