Building a Sticky Header with React MUI App Bar: Complete Guide
As a front-end developer, creating a responsive and functional header is one of the first tasks you'll tackle in most projects. The header contains critical navigation elements and actions that users need to access throughout their journey on your application. MUI's App Bar component offers a powerful solution for building sticky headers that remain accessible as users scroll through your content.
In this guide, I'll walk you through creating a professional sticky header with navigation and action elements using MUI's App Bar component. We'll cover everything from basic implementation to advanced customization techniques that I've refined over years of React development.
What You'll Learn
By the end of this tutorial, you'll know how to:
- Implement a basic App Bar with proper positioning
- Add responsive navigation with dropdowns
- Incorporate action buttons and user account features
- Handle scroll events to create sticky behavior
- Customize the App Bar's appearance with theming
- Implement accessibility best practices
- Troubleshoot common issues with App Bar implementations
Understanding MUI's App Bar Component
The App Bar (formerly Toolbar in Material-UI v4) is a fundamental component that implements the top navigation pattern in Material Design. Before diving into code, let's understand what makes this component so versatile.
Core Concepts
The App Bar serves as a container for navigation elements, branding, and actions. It's designed to be positioned at the top of your application, though it can be adapted for different layouts. The component is built on the Paper component and extends its properties, giving you access to elevation, color, and other visual customizations.
What makes the App Bar particularly powerful is its integration with other MUI components like Toolbar, IconButton, and Menu. This ecosystem allows you to create complex navigation patterns while maintaining a consistent design language.
Component Anatomy
The App Bar itself is fairly simple, but becomes powerful when combined with other components:
// Basic App Bar structure
<AppBar position="static">
<Toolbar>
<IconButton edge="start" color="inherit" aria-label="menu">
<MenuIcon />
</IconButton>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
My Application
</Typography>
<Button color="inherit">Login</Button>
</Toolbar>
</AppBar>
App Bar API Deep Dive
Understanding the available props and customization options is essential for leveraging the full power of the App Bar component.
Essential Props
The App Bar component accepts all the properties of the Paper component plus a few specific ones:
Prop | Type | Default | Description |
---|---|---|---|
position | string | 'fixed' | Controls positioning strategy ('fixed', 'absolute', 'sticky', 'static', 'relative') |
color | string | 'primary' | The color of the component ('default', 'inherit', 'primary', 'secondary', 'transparent') |
elevation | number | 4 | Shadow depth, corresponds to dp in the Material Design spec |
enableColorOnDark | boolean | false | If true, the color will be applied even in dark mode |
sx | object | The system prop that allows defining system overrides as well as additional CSS styles |
Position Property Explained
The position
prop is particularly important for creating sticky headers:
- fixed: The App Bar is positioned relative to the viewport, staying in place during scrolling
- sticky: The App Bar behaves like a normal element until scrolled to a threshold, then sticks to the top
- static: Normal document flow (default HTML behavior)
- absolute: Positioned relative to the nearest positioned ancestor
- relative: Positioned according to normal document flow
For a sticky header that's always visible, fixed
is typically the best choice. For a header that appears after scrolling down a bit, you'll need to combine sticky
with custom scroll logic.
Color System Integration
The App Bar integrates with MUI's theming system, allowing you to use predefined colors or custom ones:
// Using predefined colors
<AppBar color="primary" /> // Uses theme.palette.primary.main
<AppBar color="secondary" /> // Uses theme.palette.secondary.main
// Using custom colors with sx prop
<AppBar sx={{ bgcolor: 'success.main' }} /> // Uses theme.palette.success.main
<AppBar sx={{ bgcolor: '#ff5722' }} /> // Uses custom hex color
Customization Options
The App Bar can be customized in several ways:
- Theme Overrides: Global customization through the theme
- Styled Components: Creating a custom-styled version
- SX Prop: Inline styling with access to the theme
- CSS Classes: Traditional CSS customization
Here's an example of theme customization:
// In your theme configuration
const theme = createTheme({
components: {
MuiAppBar: {
styleOverrides: {
root: {
backgroundColor: '#2c3e50',
boxShadow: '0 3px 5px rgba(0,0,0,0.2)',
},
},
},
},
});
Step-by-Step: Building a Sticky Header with App Bar
Let's build a complete sticky header with navigation and action elements. I'll break this down into manageable steps.
Step 1: Setting Up Your Project
First, ensure you have the necessary dependencies installed:
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
These packages provide the core MUI components, icons, and styling solutions you'll need.
Step 2: Creating a Basic App Bar
Let's start with a simple App Bar implementation:
import React from 'react';
import {
AppBar,
Toolbar,
Typography,
Button,
IconButton
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
function Header() {
return (
<AppBar position="fixed">
<Toolbar>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
My Application
</Typography>
<Button color="inherit">Login</Button>
</Toolbar>
</AppBar>
);
}
export default Header;
This creates a basic App Bar with:
- A menu icon button on the left
- The application title in the center
- A login button on the right
The position="fixed"
prop ensures the App Bar stays at the top of the screen when scrolling. The flexGrow: 1
on the Typography component pushes the login button to the right.
Step 3: Adding Content Offset
When using a fixed App Bar, you need to add padding or margin to your content to prevent it from being hidden behind the header:
import React from 'react';
import { Box, CssBaseline } from '@mui/material';
import Header from './Header';
function App() {
return (
<>
<CssBaseline />
<Header />
<Box component="main" sx={{
flexGrow: 1,
p: 3,
mt: ['48px', '56px', '64px'] // Responsive top margin for different breakpoints
}}>
{/* Your page content */}
<Typography paragraph>
Lorem ipsum dolor sit amet, consectetur adipiscing elit...
</Typography>
{/* More content */}
</Box>
</>
);
}
The top margin (mt
) is set to match the height of the App Bar, which varies at different breakpoints. The CssBaseline
component normalizes styles across browsers.
Step 4: Adding Responsive Navigation Links
Now, let's enhance our header with navigation links that adapt to different screen sizes:
import React, { useState } from 'react';
import {
AppBar,
Toolbar,
Typography,
Button,
IconButton,
Box,
Menu,
MenuItem,
useMediaQuery,
useTheme
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
function Header() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [anchorEl, setAnchorEl] = useState(null);
// Navigation items
const navItems = [
{ label: 'Home', path: '/' },
{ label: 'Products', path: '/products' },
{ label: 'Services', path: '/services' },
{ label: 'About', path: '/about' },
{ label: 'Contact', path: '/contact' },
];
const handleMenuOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
return (
<AppBar position="fixed">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: isMobile ? 1 : 0 }}>
My Application
</Typography>
{isMobile ? (
<>
<IconButton
size="large"
edge="end"
color="inherit"
aria-label="menu"
onClick={handleMenuOpen}
>
<MenuIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
{navItems.map((item) => (
<MenuItem key={item.path} onClick={handleMenuClose}>
{item.label}
</MenuItem>
))}
</Menu>
</>
) : (
<Box sx={{ display: 'flex', flexGrow: 1, ml: 2 }}>
{navItems.map((item) => (
<Button
key={item.path}
color="inherit"
sx={{ mx: 1 }}
>
{item.label}
</Button>
))}
</Box>
)}
<Button color="inherit">Login</Button>
</Toolbar>
</AppBar>
);
}
export default Header;
In this enhanced version:
- We use
useMediaQuery
to detect mobile screens - On desktop, navigation links appear as buttons in the App Bar
- On mobile, they collapse into a menu accessible via an icon button
- The navigation items are defined in an array, making it easy to add or remove items
Step 5: Implementing Dropdown Menus for Complex Navigation
For more complex navigation structures, let's add dropdown menus to some of the navigation items:
import React, { useState } from 'react';
import {
AppBar,
Toolbar,
Typography,
Button,
IconButton,
Box,
Menu,
MenuItem,
useMediaQuery,
useTheme,
Popper,
Grow,
Paper,
ClickAwayListener,
MenuList
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
function Header() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [mobileAnchorEl, setMobileAnchorEl] = useState(null);
const [productsAnchorEl, setProductsAnchorEl] = useState(null);
// Navigation items
const navItems = [
{ label: 'Home', path: '/' },
{
label: 'Products',
path: '/products',
hasDropdown: true,
dropdownItems: [
{ label: 'Software', path: '/products/software' },
{ label: 'Hardware', path: '/products/hardware' },
{ label: 'Services', path: '/products/services' },
]
},
{ label: 'About', path: '/about' },
{ label: 'Contact', path: '/contact' },
];
const handleMobileMenuOpen = (event) => {
setMobileAnchorEl(event.currentTarget);
};
const handleMobileMenuClose = () => {
setMobileAnchorEl(null);
};
const handleProductsMenuOpen = (event) => {
setProductsAnchorEl(event.currentTarget);
};
const handleProductsMenuClose = () => {
setProductsAnchorEl(null);
};
return (
<AppBar position="fixed">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: isMobile ? 1 : 0 }}>
My Application
</Typography>
{isMobile ? (
<>
<IconButton
size="large"
edge="end"
color="inherit"
aria-label="menu"
onClick={handleMobileMenuOpen}
>
<MenuIcon />
</IconButton>
<Menu
anchorEl={mobileAnchorEl}
open={Boolean(mobileAnchorEl)}
onClose={handleMobileMenuClose}
>
{navItems.map((item) => (
item.hasDropdown ? (
item.dropdownItems.map(subItem => (
<MenuItem
key={subItem.path}
onClick={handleMobileMenuClose}
sx={{ pl: 4 }} // Indent submenu items
>
{subItem.label}
</MenuItem>
))
) : (
<MenuItem key={item.path} onClick={handleMobileMenuClose}>
{item.label}
</MenuItem>
)
))}
</Menu>
</>
) : (
<Box sx={{ display: 'flex', flexGrow: 1, ml: 2 }}>
{navItems.map((item) => (
item.hasDropdown ? (
<Box key={item.path} sx={{ position: 'relative' }}>
<Button
color="inherit"
onClick={handleProductsMenuOpen}
endIcon={<ArrowDropDownIcon />}
sx={{ mx: 1 }}
>
{item.label}
</Button>
<Popper
open={Boolean(productsAnchorEl)}
anchorEl={productsAnchorEl}
role={undefined}
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper elevation={3}>
<ClickAwayListener onClickAway={handleProductsMenuClose}>
<MenuList autoFocusItem={Boolean(productsAnchorEl)}>
{item.dropdownItems.map(subItem => (
<MenuItem key={subItem.path} onClick={handleProductsMenuClose}>
{subItem.label}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</Box>
) : (
<Button
key={item.path}
color="inherit"
sx={{ mx: 1 }}
>
{item.label}
</Button>
)
))}
</Box>
)}
<Button color="inherit">Login</Button>
</Toolbar>
</AppBar>
);
}
export default Header;
This implementation adds:
- A dropdown menu for the "Products" navigation item
- Different handling for mobile vs. desktop views
- Proper keyboard navigation and accessibility features with
Popper
andMenuList
- Transition effects using the
Grow
component
Step 6: Adding User Account Features
Let's enhance our header with user account features like an avatar and a profile menu:
import React, { useState } from 'react';
import {
AppBar,
Toolbar,
Typography,
Button,
IconButton,
Box,
Menu,
MenuItem,
useMediaQuery,
useTheme,
Avatar,
Divider,
ListItemIcon,
Tooltip
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import Settings from '@mui/icons-material/Settings';
import Logout from '@mui/icons-material/Logout';
import PersonIcon from '@mui/icons-material/Person';
function Header({ isLoggedIn = false, user = null }) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [mobileAnchorEl, setMobileAnchorEl] = useState(null);
const [userMenuAnchorEl, setUserMenuAnchorEl] = useState(null);
// Navigation items
const navItems = [
{ label: 'Home', path: '/' },
{ label: 'Products', path: '/products' },
{ label: 'About', path: '/about' },
{ label: 'Contact', path: '/contact' },
];
const handleMobileMenuOpen = (event) => {
setMobileAnchorEl(event.currentTarget);
};
const handleMobileMenuClose = () => {
setMobileAnchorEl(null);
};
const handleUserMenuOpen = (event) => {
setUserMenuAnchorEl(event.currentTarget);
};
const handleUserMenuClose = () => {
setUserMenuAnchorEl(null);
};
const handleLogout = () => {
// Implement your logout logic here
handleUserMenuClose();
};
return (
<AppBar position="fixed">
<Toolbar>
{isMobile && (
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
onClick={handleMobileMenuOpen}
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
)}
<Typography variant="h6" component="div" sx={{ flexGrow: isMobile ? 1 : 0 }}>
My Application
</Typography>
{!isMobile && (
<Box sx={{ display: 'flex', flexGrow: 1, ml: 2 }}>
{navItems.map((item) => (
<Button
key={item.path}
color="inherit"
sx={{ mx: 1 }}
>
{item.label}
</Button>
))}
</Box>
)}
{isLoggedIn ? (
<>
<Tooltip title="Account settings">
<IconButton
onClick={handleUserMenuOpen}
size="small"
sx={{ ml: 2 }}
aria-controls={Boolean(userMenuAnchorEl) ? 'account-menu' : undefined}
aria-haspopup="true"
aria-expanded={Boolean(userMenuAnchorEl) ? 'true' : undefined}
>
{user?.photoURL ? (
<Avatar
src={user.photoURL}
alt={user.displayName || 'User'}
sx={{ width: 32, height: 32 }}
/>
) : (
<Avatar sx={{ width: 32, height: 32, bgcolor: 'secondary.main' }}>
<AccountCircleIcon />
</Avatar>
)}
</IconButton>
</Tooltip>
<Menu
anchorEl={userMenuAnchorEl}
id="account-menu"
open={Boolean(userMenuAnchorEl)}
onClose={handleUserMenuClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{
elevation: 3,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
mt: 1.5,
'& .MuiAvatar-root': {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}}
>
<MenuItem onClick={handleUserMenuClose}>
<ListItemIcon>
<PersonIcon fontSize="small" />
</ListItemIcon>
My Profile
</MenuItem>
<MenuItem onClick={handleUserMenuClose}>
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
Settings
</MenuItem>
<Divider />
<MenuItem onClick={handleLogout}>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
Logout
</MenuItem>
</Menu>
</>
) : (
<Button color="inherit">Login</Button>
)}
{/* Mobile navigation menu */}
<Menu
anchorEl={mobileAnchorEl}
open={Boolean(mobileAnchorEl)}
onClose={handleMobileMenuClose}
>
{navItems.map((item) => (
<MenuItem key={item.path} onClick={handleMobileMenuClose}>
{item.label}
</MenuItem>
))}
</Menu>
</Toolbar>
</AppBar>
);
}
export default Header;
This implementation:
- Adds conditional rendering based on authentication state
- Includes a user avatar that opens a menu with profile options
- Uses
Tooltip
for better UX - Adds icons to menu items for visual clarity
- Includes a stylized menu with a pointer arrow
Step 7: Creating a Scroll-Aware Sticky Header
Let's enhance our header to change its appearance on scroll:
import React, { useState, useEffect } from 'react';
import {
AppBar,
Toolbar,
Typography,
Button,
IconButton,
Box,
useMediaQuery,
useTheme,
useScrollTrigger,
Slide,
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
function HideOnScroll(props) {
const { children } = props;
// Note: threshold determines how far to scroll before triggering
const trigger = useScrollTrigger({
threshold: 100,
});
return (
<Slide appear={false} direction="down" in={!trigger}>
{children}
</Slide>
);
}
function Header() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [scrolled, setScrolled] = useState(false);
// Navigation items
const navItems = [
{ label: 'Home', path: '/' },
{ label: 'Products', path: '/products' },
{ label: 'About', path: '/about' },
{ label: 'Contact', path: '/contact' },
];
// Track scroll position to change header appearance
useEffect(() => {
const handleScroll = () => {
const isScrolled = window.scrollY > 50;
if (isScrolled !== scrolled) {
setScrolled(isScrolled);
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [scrolled]);
return (
<HideOnScroll>
<AppBar
position="fixed"
sx={{
bgcolor: scrolled ? 'rgba(25, 118, 210, 0.95)' : 'primary.main',
height: scrolled ? 56 : 64,
transition: 'all 0.3s ease',
boxShadow: scrolled ? 3 : 1,
}}
>
<Toolbar sx={{
minHeight: 'unset !important',
height: '100%',
transition: 'all 0.3s ease',
}}>
{isMobile && (
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
)}
<Typography
variant={scrolled ? "h6" : "h5"}
component="div"
sx={{
flexGrow: isMobile ? 1 : 0,
transition: 'all 0.3s ease',
}}
>
My Application
</Typography>
{!isMobile && (
<Box sx={{ display: 'flex', flexGrow: 1, ml: 2 }}>
{navItems.map((item) => (
<Button
key={item.path}
color="inherit"
sx={{ mx: 1 }}
>
{item.label}
</Button>
))}
</Box>
)}
<Button color="inherit">Login</Button>
</Toolbar>
</AppBar>
</HideOnScroll>
);
}
export default Header;
This implementation:
- Uses
useScrollTrigger
to detect scroll position - Hides the header when scrolling down using the
Slide
component - Changes the header's appearance (background opacity, height, shadow) when scrolled
- Smoothly animates these changes with CSS transitions
Step 8: Adding a Progress Indicator for Long Pages
For long pages, let's add a progress indicator to the App Bar:
import React, { useState, useEffect } from 'react';
import {
AppBar,
Toolbar,
Typography,
Button,
IconButton,
Box,
useMediaQuery,
useTheme,
LinearProgress,
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
function Header() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [scrollProgress, setScrollProgress] = useState(0);
// Navigation items
const navItems = [
{ label: 'Home', path: '/' },
{ label: 'Products', path: '/products' },
{ label: 'About', path: '/about' },
{ label: 'Contact', path: '/contact' },
];
// Calculate scroll progress
useEffect(() => {
const handleScroll = () => {
const totalHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const progress = (window.scrollY / totalHeight) * 100;
setScrollProgress(progress);
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<AppBar position="fixed">
<Toolbar>
{isMobile && (
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
)}
<Typography variant="h6" component="div" sx={{ flexGrow: isMobile ? 1 : 0 }}>
My Application
</Typography>
{!isMobile && (
<Box sx={{ display: 'flex', flexGrow: 1, ml: 2 }}>
{navItems.map((item) => (
<Button
key={item.path}
color="inherit"
sx={{ mx: 1 }}
>
{item.label}
</Button>
))}
</Box>
)}
<Button color="inherit">Login</Button>
</Toolbar>
{/* Progress indicator */}
<LinearProgress
variant="determinate"
value={scrollProgress}
sx={{
height: 3,
position: 'absolute',
bottom: 0,
width: '100%',
'& .MuiLinearProgress-bar': {
transition: 'transform 0.1s linear', // Make progress update smoother
}
}}
/>
</AppBar>
);
}
export default Header;
This implementation:
- Adds a
LinearProgress
component to the bottom of the App Bar - Calculates scroll progress as a percentage of the total scrollable height
- Updates the progress indicator as the user scrolls
- Uses custom styling to make the indicator more subtle
Step 9: Accessibility Enhancements
Let's improve the accessibility of our header:
import React, { useState } from 'react';
import {
AppBar,
Toolbar,
Typography,
Button,
IconButton,
Box,
Menu,
MenuItem,
useMediaQuery,
useTheme,
VisuallyHidden,
Tooltip,
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
function Header() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [mobileAnchorEl, setMobileAnchorEl] = useState(null);
const [productsMenuAnchorEl, setProductsMenuAnchorEl] = useState(null);
// Navigation items
const navItems = [
{ label: 'Home', path: '/' },
{
label: 'Products',
path: '/products',
hasDropdown: true,
dropdownItems: [
{ label: 'Software', path: '/products/software' },
{ label: 'Hardware', path: '/products/hardware' },
{ label: 'Services', path: '/products/services' },
]
},
{ label: 'About', path: '/about' },
{ label: 'Contact', path: '/contact' },
];
const handleMobileMenuOpen = (event) => {
setMobileAnchorEl(event.currentTarget);
};
const handleMobileMenuClose = () => {
setMobileAnchorEl(null);
};
const handleProductsMenuOpen = (event) => {
setProductsMenuAnchorEl(event.currentTarget);
};
const handleProductsMenuClose = () => {
setProductsMenuAnchorEl(null);
};
// Handle keyboard navigation for dropdown
const handleProductsKeyDown = (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleProductsMenuOpen(event);
}
};
return (
<AppBar position="fixed" component="nav" aria-label="Main navigation">
<Toolbar>
{/* Skip to main content link for keyboard users */}
<VisuallyHidden>
<a href="#main-content" tabIndex={0}>
Skip to main content
</a>
</VisuallyHidden>
{isMobile && (
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="Open navigation menu"
aria-haspopup="true"
aria-expanded={Boolean(mobileAnchorEl)}
aria-controls={Boolean(mobileAnchorEl) ? 'mobile-menu' : undefined}
onClick={handleMobileMenuOpen}
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
)}
<Typography variant="h6" component="div" sx={{ flexGrow: isMobile ? 1 : 0 }}>
<a
href="/"
style={{ color: 'inherit', textDecoration: 'none' }}
aria-label="My Application - Home"
>
My Application
</a>
</Typography>
{!isMobile && (
<Box
component="nav"
sx={{ display: 'flex', flexGrow: 1, ml: 2 }}
aria-label="Main navigation"
>
{navItems.map((item) => (
item.hasDropdown ? (
<Box key={item.path}>
<Tooltip title={`Open ${item.label} menu`}>
<Button
color="inherit"
aria-haspopup="true"
aria-expanded={Boolean(productsMenuAnchorEl)}
aria-controls={Boolean(productsMenuAnchorEl) ? 'products-menu' : undefined}
onClick={handleProductsMenuOpen}
onKeyDown={handleProductsKeyDown}
endIcon={<KeyboardArrowDownIcon />}
sx={{ mx: 1 }}
>
{item.label}
</Button>
</Tooltip>
<Menu
id="products-menu"
anchorEl={productsMenuAnchorEl}
open={Boolean(productsMenuAnchorEl)}
onClose={handleProductsMenuClose}
MenuListProps={{
'aria-labelledby': 'products-button',
role: 'menu',
}}
>
{item.dropdownItems.map(subItem => (
<MenuItem
key={subItem.path}
onClick={handleProductsMenuClose}
role="menuitem"
>
{subItem.label}
</MenuItem>
))}
</Menu>
</Box>
) : (
<Button
key={item.path}
color="inherit"
sx={{ mx: 1 }}
aria-label={item.label}
role="link"
>
{item.label}
</Button>
)
))}
</Box>
)}
<Button
color="inherit"
aria-label="Login to your account"
>
Login
</Button>
{/* Mobile navigation menu */}
<Menu
id="mobile-menu"
anchorEl={mobileAnchorEl}
open={Boolean(mobileAnchorEl)}
onClose={handleMobileMenuClose}
MenuListProps={{
'aria-labelledby': 'mobile-menu-button',
role: 'menu',
}}
>
{navItems.map((item) => (
item.hasDropdown ? (
// Render dropdown items directly in mobile menu
item.dropdownItems.map(subItem => (
<MenuItem
key={subItem.path}
onClick={handleMobileMenuClose}
role="menuitem"
sx={{ pl: 4 }} // Indent submenu items
>
{subItem.label}
</MenuItem>
))
) : (
<MenuItem
key={item.path}
onClick={handleMobileMenuClose}
role="menuitem"
>
{item.label}
</MenuItem>
)
))}
</Menu>
</Toolbar>
</AppBar>
);
}
export default Header;
This implementation adds several accessibility enhancements:
- ARIA attributes for proper screen reader support
- Keyboard navigation for dropdown menus
- A "skip to main content" link for keyboard users
- Proper semantic roles for navigation elements
- Tooltips for additional context
- Improved focus management
Advanced Techniques for App Bar Customization
Now that we've built a solid foundation, let's explore some advanced techniques for customizing the App Bar.
Creating a Transparent Header That Changes on Scroll
A popular design pattern is to have a transparent header that becomes solid when scrolling:
import React, { useState, useEffect } from 'react';
import {
AppBar,
Toolbar,
Typography,
Button,
Box,
useMediaQuery,
useTheme,
} from '@mui/material';
function TransparentHeader() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [scrolled, setScrolled] = useState(false);
// Navigation items
const navItems = [
{ label: 'Home', path: '/' },
{ label: 'Products', path: '/products' },
{ label: 'About', path: '/about' },
{ label: 'Contact', path: '/contact' },
];
// Track scroll position to change header appearance
useEffect(() => {
const handleScroll = () => {
const isScrolled = window.scrollY > 20;
if (isScrolled !== scrolled) {
setScrolled(isScrolled);
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [scrolled]);
return (
<AppBar
position="fixed"
elevation={scrolled ? 4 : 0}
sx={{
bgcolor: scrolled ? 'primary.main' : 'transparent',
transition: theme.transitions.create(['background-color', 'box-shadow'], {
duration: theme.transitions.duration.standard,
}),
}}
>
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: isMobile ? 1 : 0 }}>
My Application
</Typography>
{!isMobile && (
<Box sx={{ display: 'flex', flexGrow: 1, ml: 2 }}>
{navItems.map((item) => (
<Button
key={item.path}
color="inherit"
sx={{
mx: 1,
'&:hover': {
bgcolor: scrolled ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.2)',
}
}}
>
{item.label}
</Button>
))}
</Box>
)}
<Button
variant={scrolled ? "outlined" : "contained"}
color={scrolled ? "inherit" : "secondary"}
sx={{ borderColor: scrolled ? 'white' : 'transparent' }}
>
Login
</Button>
</Toolbar>
</AppBar>
);
}
export default TransparentHeader;
This implementation:
- Starts with a transparent background
- Changes to a solid color on scroll
- Uses MUI's transition system for smooth animations
- Adjusts button styles based on scroll state
Creating a Two-Level Header
For applications with complex navigation, a two-level header can be useful:
import React, { useState } from 'react';
import {
AppBar,
Toolbar,
Typography,
Button,
IconButton,
Box,
Divider,
useMediaQuery,
useTheme,
Badge,
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import NotificationsIcon from '@mui/icons-material/Notifications';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import SearchIcon from '@mui/icons-material/Search';
function TwoLevelHeader() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// Primary navigation items
const primaryNavItems = [
{ label: 'Products', path: '/products' },
{ label: 'Solutions', path: '/solutions' },
{ label: 'Pricing', path: '/pricing' },
{ label: 'Documentation', path: '/docs' },
];
// Secondary navigation items
const secondaryNavItems = [
{ label: 'About', path: '/about' },
{ label: 'Blog', path: '/blog' },
{ label: 'Careers', path: '/careers' },
{ label: 'Support', path: '/support' },
];
return (
<AppBar position="fixed" sx={{ zIndex: theme.zIndex.drawer + 1 }}>
{/* Top toolbar with primary navigation */}
<Toolbar
sx={{
bgcolor: 'primary.dark',
minHeight: { xs: 48, sm: 56 },
px: { xs: 1, sm: 2 },
}}
>
{isMobile && (
<IconButton
size="small"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 1 }}
>
<MenuIcon />
</IconButton>
)}
<Typography
variant={isMobile ? "body1" : "h6"}
component="div"
sx={{
flexGrow: isMobile ? 1 : 0,
fontWeight: 'bold',
}}
>
My Application
</Typography>
{!isMobile && (
<Box sx={{ display: 'flex', flexGrow: 1, ml: 3 }}>
{primaryNavItems.map((item) => (
<Button
key={item.path}
color="inherit"
size="small"
sx={{ mx: 0.5 }}
>
{item.label}
</Button>
))}
</Box>
)}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<IconButton color="inherit" size="small" sx={{ ml: 1 }}>
<SearchIcon />
</IconButton>
<IconButton color="inherit" size="small" sx={{ ml: 1 }}>
<Badge badgeContent={4} color="error">
<NotificationsIcon />
</Badge>
</IconButton>
<IconButton color="inherit" size="small" sx={{ ml: 1 }}>
<Badge badgeContent={2} color="error">
<ShoppingCartIcon />
</Badge>
</IconButton>
</Box>
</Toolbar>
<Divider />
{/* Bottom toolbar with secondary navigation */}
{!isMobile && (
<Toolbar
variant="dense"
sx={{
bgcolor: 'primary.main',
minHeight: 40,
px: 2,
}}
>
<Box sx={{ display: 'flex', mx: 'auto' }}>
{secondaryNavItems.map((item) => (
<Button
key={item.path}
color="inherit"
size="small"
sx={{ mx: 1, textTransform: 'none' }}
>
{item.label}
</Button>
))}
</Box>
</Toolbar>
)}
</AppBar>
);
}
export default TwoLevelHeader;
This implementation:
- Creates a two-level header with different background colors
- Places primary navigation in the top bar
- Places secondary navigation in the bottom bar
- Hides the secondary bar on mobile devices
- Includes action icons with badge notifications
Creating a Sidebar-Integrated Header
For dashboard-style applications, integrating the header with a sidebar is common:
import React, { useState } from 'react';
import {
AppBar,
Toolbar,
Typography,
IconButton,
Box,
Drawer,
List,
ListItem,
ListItemIcon,
ListItemText,
Divider,
useTheme,
Avatar,
Badge,
Tooltip,
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import DashboardIcon from '@mui/icons-material/Dashboard';
import PeopleIcon from '@mui/icons-material/People';
import BarChartIcon from '@mui/icons-material/BarChart';
import LayersIcon from '@mui/icons-material/Layers';
import NotificationsIcon from '@mui/icons-material/Notifications';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
function DashboardHeader() {
const theme = useTheme();
const [open, setOpen] = useState(false);
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
const drawerWidth = 240;
return (
<>
<AppBar
position="fixed"
sx={{
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
...(open && {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
}),
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
sx={{
marginRight: 5,
...(open && { display: 'none' }),
}}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
Dashboard
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Tooltip title="Notifications">
<IconButton color="inherit" size="large">
<Badge badgeContent={4} color="error">
<NotificationsIcon />
</Badge>
</IconButton>
</Tooltip>
<Tooltip title="Account">
<IconButton color="inherit" size="large" sx={{ ml: 1 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: 'secondary.main' }}>
<AccountCircleIcon />
</Avatar>
</IconButton>
</Tooltip>
</Box>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
open={open}
sx={{
width: drawerWidth,
flexShrink: 0,
whiteSpace: 'nowrap',
boxSizing: 'border-box',
...(open && {
width: drawerWidth,
'& .MuiDrawer-paper': {
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
boxSizing: 'border-box',
},
}),
...(!open && {
'& .MuiDrawer-paper': {
width: theme.spacing(7),
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
boxSizing: 'border-box',
overflowX: 'hidden',
},
}),
}}
>
<Toolbar
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
px: [1],
}}
>
<IconButton onClick={handleDrawerClose}>
<ChevronLeftIcon />
</IconButton>
</Toolbar>
<Divider />
<List>
<ListItem button>
<ListItemIcon>
<DashboardIcon />
</ListItemIcon>
<ListItemText primary="Dashboard" />
</ListItem>
<ListItem button>
<ListItemIcon>
<PeopleIcon />
</ListItemIcon>
<ListItemText primary="Customers" />
</ListItem>
<ListItem button>
<ListItemIcon>
<BarChartIcon />
</ListItemIcon>
<ListItemText primary="Reports" />
</ListItem>
<ListItem button>
<ListItemIcon>
<LayersIcon />
</ListItemIcon>
<ListItemText primary="Integrations" />
</ListItem>
</List>
</Drawer>
{/* Main content */}
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
mt: 8,
ml: open ? drawerWidth : theme.spacing(7),
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
}}
>
{/* Your page content */}
<Typography paragraph>
Content goes here...
</Typography>
</Box>
</>
);
}
export default DashboardHeader;
This implementation:
- Creates a collapsible sidebar integrated with the App Bar
- Adjusts the App Bar width when the sidebar is opened/closed
- Uses smooth transitions for a polished UX
- Includes navigation items in the sidebar
- Provides action buttons in the App Bar
Best Practices and Common Issues
Based on my experience working with MUI's App Bar, here are some best practices and common issues to be aware of:
Best Practices
-
Use the Correct Position Value
Choose the appropriate
position
value based on your layout needs:fixed
for a header that's always visiblesticky
for a header that sticks after scrolling past itstatic
for a header that scrolls with the page
-
Handle Content Spacing
When using
position="fixed"
, remember to add padding or margin to your content to prevent it from being hidden behind the header. -
Optimize for Performance
For scroll-aware headers, use throttling or debouncing to prevent excessive re-renders:
import { throttle } from 'lodash';
// In your component
useEffect(() => {
const handleScroll = throttle(() => {
const isScrolled = window.scrollY > 50;
if (isScrolled !== scrolled) {
setScrolled(isScrolled);
}
}, 100); // Execute at most once every 100ms
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
handleScroll.cancel(); // Cancel any pending executions
};
}, [scrolled]);
-
Responsive Design Considerations
Always design your App Bar with mobile users in mind:
- Use
useMediaQuery
to adapt to different screen sizes - Collapse navigation into a menu on small screens
- Consider hiding less important elements on mobile
- Use
-
Maintain Accessibility
Ensure your App Bar is accessible:
- Add proper ARIA attributes
- Ensure sufficient color contrast
- Make sure all interactive elements are keyboard accessible
- Include a "skip to main content" link
Common Issues and Solutions
-
Issue: Content Hidden Behind Fixed App Bar
Solution: Add padding to your main content equal to the App Bar height:
<Box component="main" sx={{ pt: '64px' }}>
{/* Content */}
</Box>
-
Issue: App Bar Height Changes on Scroll Causing Layout Shifts
Solution: Use a placeholder element with the same height:
<>
<AppBar position="fixed" sx={{ height: scrolled ? 56 : 64 }}>
{/* App Bar content */}
</AppBar>
<Box sx={{ height: scrolled ? 56 : 64 }} />
{/* Main content */}
</>
-
Issue: Dropdown Menus Cut Off by Container Boundaries
Solution: Use the
disablePortal={false}
prop on Menu components:
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
disablePortal={false} // Ensures menu is rendered in a portal
>
{/* Menu items */}
</Menu>
-
Issue: Unexpected Re-renders on Scroll
Solution: Use the
useMemo
hook for computed values:
const appBarStyle = useMemo(() => ({
backgroundColor: scrolled ? 'rgba(25, 118, 210, 0.95)' : 'transparent',
boxShadow: scrolled ? 3 : 0,
}), [scrolled]);
return (
<AppBar position="fixed" sx={appBarStyle}>
{/* App Bar content */}
</AppBar>
);
-
Issue: App Bar Not Visible in High Z-index Contexts
Solution: Adjust the z-index of the App Bar:
<AppBar position="fixed" sx={{ zIndex: theme.zIndex.drawer + 1 }}>
{/* App Bar content */}
</AppBar>
Wrapping Up
In this guide, we've explored how to build a professional sticky header using MUI's App Bar component. We've covered everything from basic implementation to advanced customization techniques, accessibility considerations, and common issues.
The App Bar is a versatile component that can be adapted to a wide range of use cases, from simple navigation headers to complex dashboard interfaces. By leveraging MUI's theming system and combining the App Bar with other components, you can create headers that are both functional and visually appealing.
Remember to prioritize performance, accessibility, and responsive design when implementing your headers. With the techniques covered in this guide, you should be well-equipped to create sticky headers that enhance the user experience of your React applications.