Building a Notification Indicator for Messaging UI with React MUI Badge
As a front-end developer, creating an effective notification system is crucial for any messaging application. The MUI Badge component offers a powerful yet elegant solution for displaying notification counts and status indicators. In this article, I'll walk you through implementing a comprehensive notification system for a messaging UI using MUI's Badge component, covering everything from basic implementation to advanced customization techniques.
What You'll Learn
In this guide, we'll explore:
- Understanding the MUI Badge component and its core functionality
- Setting up a complete messaging UI with notification indicators
- Customizing Badge appearance with variants, colors, and positioning
- Creating animated notification badges for better user experience
- Implementing real-time notification updates with WebSockets
- Handling accessibility concerns for notification indicators
- Advanced patterns for complex notification scenarios
By the end of this article, you'll have all the knowledge needed to implement a professional-grade notification system in your React applications.
Understanding the MUI Badge Component
The Badge component in Material-UI is a small counter or status indicator that appears as a colored dot, number, or icon positioned at the corner of another element. It's particularly useful for representing unread messages, notifications, or status indicators.
Core Functionality and Concept
The Badge component wraps around another element (like an icon, button, or avatar) and displays a small badge at one of its corners. This badge can contain a number, text, or simply be a dot indicator. It's designed to draw attention to a particular element and provide additional information without taking up much space.
Badges follow Material Design principles, making them visually consistent with other UI elements while providing important contextual information to users. In messaging applications, badges typically indicate unread message counts, online status, or other notification states.
Badge Component Props Breakdown
The Badge component offers extensive customization through its props. Let's examine the most important ones:
Prop | Type | Default | Description |
---|---|---|---|
anchorOrigin | vertical: 'top' | 'bottom', horizontal: 'left' | 'right' | vertical: 'top', horizontal: 'right' | Position where the badge should appear relative to the wrapped element |
badgeContent | node | - | The content rendered within the badge |
children | node | - | The element that the badge will be added to |
color | 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' | string | 'default' | The color of the badge |
component | elementType | 'span' | The component used for the root node |
invisible | bool | false | Controls whether the badge is visible |
max | number | 99 | Max count to show |
overlap | 'circular' | 'rectangular' | 'rectangular' | How the badge should overlap its children |
showZero | bool | false | Controls the visibility when badgeContent is zero |
variant | 'dot' | 'standard' | string | 'standard' | The variant to use |
sx | object | - | The system prop that allows defining system overrides as well as additional CSS styles |
Badge Variants and Visual Configurations
The Badge component offers two primary variants:
- Standard: Displays a numerical value or text (default)
- Dot: Shows a small dot without any content
These variants can be combined with different colors to represent various notification types or urgency levels. For example, you might use a red badge for important alerts, green for status indicators, or blue for general notifications.
// Standard badge with count
<Badge badgeContent={4} color="primary">
<MailIcon />
</Badge>
// Dot variant
<Badge variant="dot" color="error">
<NotificationsIcon />
</Badge>
Controlled vs Uncontrolled Usage
Like many React components, Badge can be used in both controlled and uncontrolled patterns:
Uncontrolled Badge: Set the badgeContent
directly with a static value:
<Badge badgeContent={5} color="primary">
<MailIcon />
</Badge>
Controlled Badge: Manage the badge content through state:
const [messageCount, setMessageCount] = useState(5);
// Later in your component
<Badge badgeContent={messageCount} color="primary">
<MailIcon />
</Badge>
The controlled approach is particularly useful for dynamic notifications where counts might change based on user actions or server events.
Setting Up Your Project
Before we dive into creating our notification system, let's set up a new React project with the necessary dependencies.
Installing Dependencies
We'll need React, Material-UI, and some additional packages for our messaging UI:
# Using npm
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
# Using yarn
yarn add @mui/material @mui/icons-material @emotion/react @emotion/styled
Project Structure
Let's organize our project with a clean structure:
src/
├── components/
│ ├── MessageList.jsx
│ ├── Conversation.jsx
│ ├── ChatHeader.jsx
│ ├── NotificationBadge.jsx
│ └── MessageInput.jsx
├── hooks/
│ └── useNotifications.js
├── App.jsx
└── index.js
This structure separates our UI components and business logic, making the code more maintainable as the application grows.
Building a Basic Notification Badge
Let's start by creating a simple notification badge component that we can reuse throughout our application.
Creating a Reusable NotificationBadge Component
First, let's build a reusable component that handles common notification badge scenarios:
// src/components/NotificationBadge.jsx
import React from 'react';
import { Badge } from '@mui/material';
const NotificationBadge = ({
count = 0,
max = 99,
color = 'primary',
variant = 'standard',
showZero = false,
children,
invisible,
...props
}) => {
// Determine if badge should be invisible
const isInvisible = invisible !== undefined
? invisible
: (count === 0 && !showZero);
return (
<Badge
badgeContent={count}
color={color}
max={max}
variant={variant}
invisible={isInvisible}
{...props} >
{children}
</Badge>
);
};
export default NotificationBadge;
This component provides sensible defaults while allowing for customization through props. The isInvisible
logic handles both explicit control through the invisible
prop and automatic hiding when the count is zero (unless showZero
is true).
Integrating the Badge with Navigation Icons
Now, let's integrate our notification badge with navigation icons for a messaging app:
// src/components/ChatHeader.jsx
import React from 'react';
import {
AppBar,
Toolbar,
Typography,
IconButton,
Box
} from '@mui/material';
import {
Menu as MenuIcon,
Notifications as NotificationsIcon,
Mail as MailIcon,
MoreVert as MoreIcon
} from '@mui/icons-material';
import NotificationBadge from './NotificationBadge';
const ChatHeader = ({ messageCount = 0, notificationCount = 0 }) => {
return (
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }} >
<MenuIcon />
</IconButton>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Messaging App
</Typography>
<Box sx={{ display: 'flex' }}>
<IconButton color="inherit" aria-label="show new messages">
<NotificationBadge count={messageCount} color="error">
<MailIcon />
</NotificationBadge>
</IconButton>
<IconButton color="inherit" aria-label="show notifications">
<NotificationBadge count={notificationCount} color="error">
<NotificationsIcon />
</NotificationBadge>
</IconButton>
<IconButton
edge="end"
color="inherit"
aria-label="more options"
>
<MoreIcon />
</IconButton>
</Box>
</Toolbar>
</AppBar>
);
};
export default ChatHeader;
This header component includes notification badges for both messages and general notifications, providing visual feedback to users about unread items.
Creating a Complete Messaging UI with Notification Indicators
Now that we have our basic badge component, let's build a complete messaging UI that uses notification badges in various contexts.
Building the Message List Component
First, let's create a message list component that displays conversations with notification badges for unread messages:
// src/components/MessageList.jsx
import React from 'react';
import {
List,
ListItem,
ListItemAvatar,
ListItemText,
Avatar,
Divider,
Typography,
Box
} from '@mui/material';
import NotificationBadge from './NotificationBadge';
const MessageList = ({ conversations = [] }) => {
return (
<List sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}>
{conversations.map((conversation, index) => (
<React.Fragment key={conversation.id}>
<ListItem alignItems="flex-start" button>
<ListItemAvatar>
<Box sx={{ position: 'relative' }}>
<NotificationBadge
color={conversation.online ? 'success' : 'default'}
variant="dot"
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
overlap="circular"
invisible={!conversation.online}
sx={{
'& .MuiBadge-badge': {
border: '2px solid white',
}
}} >
<Avatar alt={conversation.name} src={conversation.avatar} />
</NotificationBadge>
</Box>
</ListItemAvatar>
<ListItemText
primary={
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography component="span" variant="body1">
{conversation.name}
</Typography>
<Typography component="span" variant="caption" color="text.secondary">
{conversation.lastMessageTime}
</Typography>
</Box>
}
secondary={
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography
component="span"
variant="body2"
color="text.primary"
sx={{
display: 'inline',
maxWidth: '70%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{conversation.lastMessage}
</Typography>
{conversation.unreadCount > 0 && (
<NotificationBadge
count={conversation.unreadCount}
color="primary"
sx={{ marginLeft: 1 }}
>
<Box sx={{ width: 8, height: 8 }} /> {/* Empty box for badge to attach to */}
</NotificationBadge>
)}
</Box>
}
/>
</ListItem>
{index < conversations.length - 1 && <Divider variant="inset" component="li" />}
</React.Fragment>
))}
</List>
);
};
export default MessageList;
This component showcases two different uses of the Badge:
- A dot badge indicating online status on the user's avatar
- A standard badge showing unread message counts
Creating the Conversation Component
Next, let's create a conversation component that displays individual messages:
// src/components/Conversation.jsx
import React from 'react';
import { Box, Typography, Avatar, Paper } from '@mui/material';
import DoneAllIcon from '@mui/icons-material/DoneAll';
import NotificationBadge from './NotificationBadge';
const Message = ({ message, isCurrentUser }) => {
return (
<Box
sx={{
display: 'flex',
justifyContent: isCurrentUser ? 'flex-end' : 'flex-start',
mb: 2,
}} >
{!isCurrentUser && (
<Avatar
src={message.avatar}
alt={message.sender}
sx={{ mr: 1, width: 32, height: 32 }}
/>
)}
<Paper
elevation={1}
sx={{
p: 1.5,
maxWidth: '70%',
borderRadius: 2,
bgcolor: isCurrentUser ? 'primary.light' : 'background.default',
color: isCurrentUser ? 'primary.contrastText' : 'text.primary',
}}
>
<Typography variant="body1">{message.text}</Typography>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', mt: 0.5 }}>
<Typography variant="caption" color={isCurrentUser ? 'primary.contrastText' : 'text.secondary'} sx={{ opacity: 0.7 }}>
{message.time}
</Typography>
{isCurrentUser && (
<NotificationBadge
badgeContent={
message.read ? (
<DoneAllIcon sx={{ fontSize: 12, color: 'success.main' }} />
) : null
}
invisible={!message.sent}
sx={{ ml: 0.5 }}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
>
<Box sx={{ width: 4, height: 12 }} />
</NotificationBadge>
)}
</Box>
</Paper>
</Box>
);
};
const Conversation = ({ messages = [] }) => {
return (
<Box sx={{ p: 2, height: '70vh', overflowY: 'auto' }}>
{messages.map((message) => (
<Message
key={message.id}
message={message}
isCurrentUser={message.isCurrentUser}
/>
))}
</Box>
);
};
export default Conversation;
This component uses the Badge in an interesting way - to display read receipts for messages. We're using a custom badge content (a checkmark icon) to indicate when messages have been read.
Building the Message Input Component
Let's create a message input component with typing indicators:
// src/components/MessageInput.jsx
import React, { useState } from 'react';
import {
Box,
TextField,
IconButton,
Paper,
Typography
} from '@mui/material';
import {
Send as SendIcon,
AttachFile as AttachFileIcon,
EmojiEmotions as EmojiIcon
} from '@mui/icons-material';
import NotificationBadge from './NotificationBadge';
const MessageInput = ({ onSendMessage, typingUsers = [] }) => {
const [message, setMessage] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (message.trim()) {
onSendMessage(message);
setMessage('');
}
};
return (
<Paper
sx={{
p: 2,
borderTop: '1px solid',
borderColor: 'divider',
position: 'relative'
}} >
{typingUsers.length > 0 && (
<Box sx={{ position: 'absolute', top: -28, left: 16 }}>
<NotificationBadge
badgeContent={typingUsers.length > 1 ? typingUsers.length : null}
color="secondary"
overlap="circular"
invisible={typingUsers.length === 0}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}} >
<Paper
elevation={2}
sx={{
px: 2,
py: 0.5,
borderRadius: 4,
bgcolor: 'secondary.light',
display: 'flex',
alignItems: 'center'
}} >
<Typography variant="caption" color="secondary.dark">
{typingUsers.length === 1
? `${typingUsers[0]} is typing...`
: 'Multiple people are typing...'}
</Typography>
</Paper>
</NotificationBadge>
</Box>
)}
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', alignItems: 'center' }}>
<IconButton color="primary" aria-label="attach file">
<AttachFileIcon />
</IconButton>
<TextField
fullWidth
placeholder="Type a message"
variant="outlined"
size="small"
value={message}
onChange={(e) => setMessage(e.target.value)}
sx={{ mx: 1 }}
/>
<IconButton color="primary" aria-label="insert emoji">
<EmojiIcon />
</IconButton>
<IconButton
color="primary"
aria-label="send message"
type="submit"
disabled={!message.trim()}
>
<SendIcon />
</IconButton>
</Box>
</Paper>
);
};
export default MessageInput;
Here, we use the Badge to show a typing indicator with a count of how many people are currently typing.
Assembling the App Component
Now, let's put everything together in our main App component:
// src/App.jsx
import React, { useState, useEffect } from 'react';
import { Box, Paper, CssBaseline, ThemeProvider, createTheme } from '@mui/material';
import ChatHeader from './components/ChatHeader';
import MessageList from './components/MessageList';
import Conversation from './components/Conversation';
import MessageInput from './components/MessageInput';
// Sample data
const sampleConversations = [
{
id: 1,
name: 'John Doe',
avatar: 'https://mui.com/static/images/avatar/1.jpg',
lastMessage: 'Hey, how are you doing?',
lastMessageTime: '10:30 AM',
unreadCount: 3,
online: true
},
{
id: 2,
name: 'Jane Smith',
avatar: 'https://mui.com/static/images/avatar/2.jpg',
lastMessage: 'The project deadline is tomorrow!',
lastMessageTime: 'Yesterday',
unreadCount: 0,
online: false
},
{
id: 3,
name: 'Team Developers',
avatar: 'https://mui.com/static/images/avatar/3.jpg',
lastMessage: 'Alice: We need to discuss the new feature',
lastMessageTime: 'Yesterday',
unreadCount: 5,
online: true
}
];
const sampleMessages = [
{
id: 1,
text: 'Hey there!',
sender: 'John',
avatar: 'https://mui.com/static/images/avatar/1.jpg',
time: '10:30 AM',
isCurrentUser: false,
sent: true,
read: true
},
{
id: 2,
text: 'Hi! How are you?',
sender: 'You',
time: '10:31 AM',
isCurrentUser: true,
sent: true,
read: true
},
{
id: 3,
text: 'I'm good, thanks! Just working on that React project we discussed.',
sender: 'John',
avatar: 'https://mui.com/static/images/avatar/1.jpg',
time: '10:32 AM',
isCurrentUser: false,
sent: true,
read: true
},
{
id: 4,
text: 'How's it going with the notification system?',
sender: 'You',
time: '10:33 AM',
isCurrentUser: true,
sent: true,
read: false
}
];
// Create theme
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
},
secondary: {
main: '#9c27b0',
},
},
});
const App = () => {
const [totalUnread, setTotalUnread] = useState(0);
const [typingUsers, setTypingUsers] = useState([]);
// Calculate total unread messages
useEffect(() => {
const unreadCount = sampleConversations.reduce(
(sum, conversation) => sum + conversation.unreadCount,
0
);
setTotalUnread(unreadCount);
}, []);
// Simulate typing indicator
useEffect(() => {
const timer = setTimeout(() => {
setTypingUsers(['John Doe']);
const timer2 = setTimeout(() => {
setTypingUsers([]);
}, 3000);
return () => clearTimeout(timer2);
}, 2000);
return () => clearTimeout(timer);
}, []);
const handleSendMessage = (message) => {
console.log('Sent message:', message);
// In a real app, you would add the message to state
// and potentially send it to a backend
};
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<ChatHeader messageCount={totalUnread} notificationCount={2} />
<Box sx={{ display: 'flex', flexGrow: 1, overflow: 'hidden' }}>
<Paper sx={{ width: 360, borderRight: '1px solid', borderColor: 'divider' }}>
<MessageList conversations={sampleConversations} />
</Paper>
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
<Conversation messages={sampleMessages} />
<MessageInput onSendMessage={handleSendMessage} typingUsers={typingUsers} />
</Box>
</Box>
</Box>
</ThemeProvider>
);
};
export default App;
This App component brings all our UI elements together, creating a complete messaging interface with multiple notification indicators.
Advanced Badge Customization Techniques
Now that we have a functional messaging UI, let's explore advanced customization techniques for our badges.
Styling Badges with the sx Prop
The sx
prop is a powerful way to customize MUI components. Let's create some custom badge styles:
// Custom pulse animation badge
<NotificationBadge
count={5}
color="error"
sx={{
'& .MuiBadge-badge': {
animation: 'pulse 1.5s infinite',
'@keyframes pulse': {
'0%': {
transform: 'scale(1)',
},
'50%': {
transform: 'scale(1.2)',
},
'100%': {
transform: 'scale(1)',
},
},
},
}}
>
<NotificationsIcon />
</NotificationBadge>
// Custom shaped badge
<NotificationBadge
count={7}
sx={{
"& .MuiBadge-badge": {
borderRadius: "4px",
height: "16px",
minWidth: "16px",
padding: "0 4px",
},
}}
>
<MailIcon />
</NotificationBadge>
// Badge with outline
<NotificationBadge
count={3}
color="primary"
sx={{
"& .MuiBadge-badge": {
border: "2px solid white",
padding: "0 6px",
},
}}
>
<Avatar alt="User" src="/user-avatar.jpg" />
</NotificationBadge>
These examples demonstrate how to create animated badges, custom-shaped badges, and badges with outlines.
Custom Badge Positioning
You can precisely control the position of badges using the anchorOrigin
prop combined with custom styling:
// Top-left positioned badge
<NotificationBadge
count={12}
color="secondary"
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<FolderIcon />
</NotificationBadge>
// Custom positioned badge with offset
<NotificationBadge
count={9}
color="primary"
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
sx={{
"& .MuiBadge-badge": {
bottom: 5,
right: 5,
},
}}
>
<AccountCircleIcon />
</NotificationBadge>
Creating a Customized Theme for Badges
For consistent badge styling across your application, you can customize the Badge component in your theme:
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
components: {
MuiBadge: {
styleOverrides: {
badge: {
fontSize: '0.75rem',
fontWeight: 'bold',
borderRadius: 12,
padding: '0 6px',
minWidth: 20,
height: 20,
// Add custom styles for specific colors
'&.MuiBadge-colorPrimary': {
background: 'linear-gradient(45deg, #2196F3 30%, #21CBF3 90%)',
boxShadow: '0 3px 5px 2px rgba(33, 203, 243, .3)',
},
'&.MuiBadge-colorError': {
background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
},
},
},
},
},
});
This theme customization creates consistent badge styling throughout your application, with special gradient effects for primary and error badges.
Implementing Real-Time Notification Updates
A messaging app needs to update notifications in real-time. Let's create a custom hook to handle this functionality.
Creating a useNotifications Hook
// src/hooks/useNotifications.js
import { useState, useEffect, useCallback } from 'react';
const useNotifications = (initialCount = 0) => {
const [count, setCount] = useState(initialCount);
const [lastNotification, setLastNotification] = useState(null);
const [isConnected, setIsConnected] = useState(false);
// Simulate WebSocket connection
useEffect(() => {
console.log('Connecting to notification service...');
// Simulate connection delay
const connectionTimer = setTimeout(() => {
setIsConnected(true);
console.log('Connected to notification service');
}, 1500);
// Cleanup function
return () => {
clearTimeout(connectionTimer);
setIsConnected(false);
console.log('Disconnected from notification service');
};
}, []);
// Simulate incoming notifications
useEffect(() => {
if (!isConnected) return;
const notificationTypes = [
'message',
'friend_request',
'mention',
'reaction'
];
const simulateIncomingNotification = () => {
const type = notificationTypes[Math.floor(Math.random() * notificationTypes.length)];
const newNotification = {
id: Date.now(),
type,
message: `New ${type} notification`,
timestamp: new Date().toISOString(),
};
setCount(prevCount => prevCount + 1);
setLastNotification(newNotification);
console.log('Received notification:', newNotification);
};
// Simulate random notifications
const notificationInterval = setInterval(() => {
// 30% chance of receiving a notification
if (Math.random() < 0.3) {
simulateIncomingNotification();
}
}, 5000);
return () => clearInterval(notificationInterval);
}, [isConnected]);
// Method to mark notifications as read
const markAsRead = useCallback((amount = null) => {
setCount(prevCount => {
if (amount === null) return 0;
return Math.max(0, prevCount - amount);
});
}, []);
// Method to add a notification manually (for testing)
const addNotification = useCallback((notificationData = {}) => {
const newNotification = {
id: Date.now(),
type: 'custom',
message: 'Custom notification',
timestamp: new Date().toISOString(),
...notificationData,
};
setCount(prevCount => prevCount + 1);
setLastNotification(newNotification);
return newNotification;
}, []);
return {
count,
lastNotification,
isConnected,
markAsRead,
addNotification,
};
};
export default useNotifications;
This hook simulates real-time notifications using timers, but in a real application, you would connect to a WebSocket service or use a polling mechanism to receive notifications from your server.
Integrating Real-Time Notifications with Our UI
Let's update our ChatHeader component to use this hook:
// Updated ChatHeader.jsx
import React, { useEffect } from 'react';
import {
AppBar,
Toolbar,
Typography,
IconButton,
Box,
Menu,
MenuItem,
Tooltip,
Fade
} from '@mui/material';
import {
Menu as MenuIcon,
Notifications as NotificationsIcon,
Mail as MailIcon,
MoreVert as MoreIcon
} from '@mui/icons-material';
import NotificationBadge from './NotificationBadge';
import useNotifications from '../hooks/useNotifications';
const ChatHeader = () => {
const messageNotifications = useNotifications(0);
const generalNotifications = useNotifications(0);
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
// Show notification tooltip when we receive a new notification
const [showTooltip, setShowTooltip] = React.useState(false);
useEffect(() => {
if (generalNotifications.lastNotification) {
setShowTooltip(true);
const timer = setTimeout(() => {
setShowTooltip(false);
}, 3000);
return () => clearTimeout(timer);
}
}, [generalNotifications.lastNotification]);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
if (generalNotifications.count > 0) {
generalNotifications.markAsRead();
}
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }} >
<MenuIcon />
</IconButton>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Messaging App
</Typography>
<Box sx={{ display: 'flex' }}>
<IconButton
color="inherit"
aria-label="show new messages"
onClick={() => messageNotifications.markAsRead()}
>
<NotificationBadge
count={messageNotifications.count}
color="error"
max={99}
sx={{
'& .MuiBadge-badge': {
right: -3,
top: 3,
},
}}
>
<MailIcon />
</NotificationBadge>
</IconButton>
<Tooltip
title={
generalNotifications.lastNotification
? generalNotifications.lastNotification.message
: ''
}
open={showTooltip}
arrow
placement="bottom"
TransitionComponent={Fade}
TransitionProps={{ timeout: 600 }}
>
<IconButton
color="inherit"
aria-label="show notifications"
onClick={handleClick}
aria-controls={open ? 'notification-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
<NotificationBadge
count={generalNotifications.count}
color="error"
sx={{
'& .MuiBadge-badge': {
animation: generalNotifications.count > 0
? 'pulse 1.5s infinite'
: 'none',
'@keyframes pulse': {
'0%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.2)' },
'100%': { transform: 'scale(1)' },
},
},
}}
>
<NotificationsIcon />
</NotificationBadge>
</IconButton>
</Tooltip>
<Menu
id="notification-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'notification-button',
}}
>
<MenuItem onClick={handleClose}>Friend Requests (2)</MenuItem>
<MenuItem onClick={handleClose}>Mentions (3)</MenuItem>
<MenuItem onClick={handleClose}>System Notifications (1)</MenuItem>
</Menu>
<IconButton
edge="end"
color="inherit"
aria-label="more options"
>
<MoreIcon />
</IconButton>
</Box>
</Toolbar>
</AppBar>
);
};
export default ChatHeader;
This updated component uses our custom hook to manage real-time notifications, displaying them with animated badges and tooltips.
Accessibility Enhancements for Notification Badges
Accessibility is crucial for any UI component, especially notification indicators. Let's enhance our NotificationBadge component to be more accessible:
// Enhanced NotificationBadge.jsx with accessibility features
import React from 'react';
import { Badge, Tooltip, VisuallyHidden } from '@mui/material';
const NotificationBadge = ({
count = 0,
max = 99,
color = 'primary',
variant = 'standard',
showZero = false,
children,
invisible,
tooltipText,
ariaLabel,
...props
}) => {
// Determine if badge should be invisible
const isInvisible = invisible !== undefined
? invisible
: (count === 0 && !showZero);
// Create an accessible label for screen readers
const getAccessibleLabel = () => {
if (variant === 'dot') {
return 'Notification indicator';
}
if (count === 0) {
return 'No notifications';
}
if (count > max) {
return `${max}+ notifications`;
}
return `${count} ${count === 1 ? 'notification' : 'notifications'}`;
};
const badgeContent = (
<>
{count}
<VisuallyHidden>{getAccessibleLabel()}</VisuallyHidden>
</>
);
const badge = (
<Badge
badgeContent={variant === 'dot' ? undefined : badgeContent}
color={color}
max={max}
variant={variant}
invisible={isInvisible}
aria-label={ariaLabel || getAccessibleLabel()}
{...props} >
{children}
</Badge>
);
// If tooltip text is provided, wrap badge in a tooltip
if (tooltipText && !isInvisible) {
return (
<Tooltip title={tooltipText} arrow>
{badge}
</Tooltip>
);
}
return badge;
};
export default NotificationBadge;
This enhanced version of our component:
- Provides proper ARIA labels for screen readers
- Includes visually hidden text for more descriptive announcements
- Adds optional tooltips for additional context
- Dynamically changes the description based on the count and variant
Keyboard Navigation for Notification Menus
Let's enhance our notification menu to support keyboard navigation:
// Keyboard-accessible notification menu
import React, { useState, useRef } from 'react';
import {
IconButton,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
Typography,
Divider
} from '@mui/material';
import {
Notifications as NotificationsIcon,
Message as MessageIcon,
People as PeopleIcon,
Settings as SettingsIcon
} from '@mui/icons-material';
import NotificationBadge from './NotificationBadge';
const NotificationMenu = ({ count = 0 }) => {
const [anchorEl, setAnchorEl] = useState(null);
const buttonRef = useRef(null);
const handleOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
// Return focus to the button when closing the menu
buttonRef.current?.focus();
};
const handleKeyDown = (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleOpen(event);
}
};
return (
<>
<IconButton
ref={buttonRef}
aria-label={`${count} notifications`}
aria-controls={Boolean(anchorEl) ? 'notification-menu' : undefined}
aria-haspopup="true"
aria-expanded={Boolean(anchorEl) ? 'true' : 'false'}
onClick={handleOpen}
onKeyDown={handleKeyDown}
color="inherit" >
<NotificationBadge
count={count}
color="error"
ariaLabel={`${count} notifications`} >
<NotificationsIcon />
</NotificationBadge>
</IconButton>
<Menu
id="notification-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'notification-button',
dense: true,
}}
>
<Typography variant="subtitle1" sx={{ px: 2, pt: 1, pb: 0.5 }}>
Notifications
</Typography>
<Divider />
<MenuItem onClick={handleClose}>
<ListItemIcon>
<MessageIcon fontSize="small" />
</ListItemIcon>
<ListItemText
primary="New message from Alex"
secondary="Hey, how's the project going?"
/>
</MenuItem>
<MenuItem onClick={handleClose}>
<ListItemIcon>
<PeopleIcon fontSize="small" />
</ListItemIcon>
<ListItemText
primary="Friend request"
secondary="Sarah wants to connect"
/>
</MenuItem>
<MenuItem onClick={handleClose}>
<ListItemIcon>
<SettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText
primary="System notification"
secondary="Your account was logged in from a new device"
/>
</MenuItem>
<Divider />
<MenuItem onClick={handleClose} dense>
<Typography variant="body2" color="primary" sx={{ width: '100%', textAlign: 'center' }}>
View all notifications
</Typography>
</MenuItem>
</Menu>
</>
);
};
export default NotificationMenu;
This component ensures proper keyboard accessibility by:
- Using proper ARIA attributes
- Supporting keyboard activation (Enter/Space)
- Managing focus correctly when opening/closing the menu
- Providing clear labels and structure
Advanced Badge Patterns for Complex Scenarios
Let's explore some advanced patterns for complex notification scenarios.
Creating a Combined Notification Badge
Sometimes you need to show multiple types of notifications in a single badge:
// CombinedNotificationBadge.jsx
import React from 'react';
import { Badge, Box, Tooltip } from '@mui/material';
import NotificationsIcon from '@mui/icons-material/Notifications';
const CombinedNotificationBadge = ({
messageCount = 0,
friendRequestCount = 0,
systemCount = 0,
onClick
}) => {
const totalCount = messageCount + friendRequestCount + systemCount;
// Generate tooltip content with breakdown
const tooltipContent = () => {
if (totalCount === 0) return 'No notifications';
const parts = [];
if (messageCount > 0) {
parts.push(`${messageCount} message${messageCount !== 1 ? 's' : ''}`);
}
if (friendRequestCount > 0) {
parts.push(`${friendRequestCount} friend request${friendRequestCount !== 1 ? 's' : ''}`);
}
if (systemCount > 0) {
parts.push(`${systemCount} system notification${systemCount !== 1 ? 's' : ''}`);
}
return parts.join(', ');
};
// Determine badge color based on content
const getBadgeColor = () => {
if (systemCount > 0) return 'error';
if (friendRequestCount > 0) return 'secondary';
return 'primary';
};
return (
<Tooltip title={tooltipContent()} arrow>
<Box sx={{ position: 'relative' }}>
<Badge
badgeContent={totalCount}
color={getBadgeColor()}
invisible={totalCount === 0}
onClick={onClick}
sx={{
'& .MuiBadge-badge': {
right: -3,
top: 3,
},
}} >
<NotificationsIcon />
</Badge>
{/* Secondary indicator for message count */}
{messageCount > 0 && friendRequestCount + systemCount > 0 && (
<Box
sx={{
position: 'absolute',
bottom: 0,
right: 0,
width: 8,
height: 8,
backgroundColor: 'primary.main',
borderRadius: '50%',
border: '1px solid white',
}}
/>
)}
</Box>
</Tooltip>
);
};
export default CombinedNotificationBadge;
This component shows a combined count of all notifications, but changes color based on priority (system notifications are most important) and adds a secondary indicator when there are multiple types of notifications.
Creating a Badge Group for Multiple Notification Types
Another approach is to group multiple badges together:
// NotificationBadgeGroup.jsx
import React from 'react';
import { Box, IconButton, Tooltip } from '@mui/material';
import {
Mail as MailIcon,
Notifications as NotificationsIcon,
People as PeopleIcon
} from '@mui/icons-material';
import NotificationBadge from './NotificationBadge';
const NotificationBadgeGroup = ({
messageCount = 0,
notificationCount = 0,
friendRequestCount = 0,
onMessageClick,
onNotificationClick,
onFriendRequestClick
}) => {
return (
<Box sx={{ display: 'flex', gap: 1 }}>
<Tooltip title={`${messageCount} messages`}>
<IconButton color="inherit" onClick={onMessageClick} aria-label={`${messageCount} messages`}>
<NotificationBadge
count={messageCount}
color="primary"
sx={{
'& .MuiBadge-badge': {
fontSize: '0.6rem',
},
}} >
<MailIcon />
</NotificationBadge>
</IconButton>
</Tooltip>
<Tooltip title={`${notificationCount} notifications`}>
<IconButton color="inherit" onClick={onNotificationClick} aria-label={`${notificationCount} notifications`}>
<NotificationBadge
count={notificationCount}
color="error"
sx={{
'& .MuiBadge-badge': {
fontSize: '0.6rem',
},
}}
>
<NotificationsIcon />
</NotificationBadge>
</IconButton>
</Tooltip>
<Tooltip title={`${friendRequestCount} friend requests`}>
<IconButton color="inherit" onClick={onFriendRequestClick} aria-label={`${friendRequestCount} friend requests`}>
<NotificationBadge
count={friendRequestCount}
color="secondary"
sx={{
'& .MuiBadge-badge': {
fontSize: '0.6rem',
},
}}
>
<PeopleIcon />
</NotificationBadge>
</IconButton>
</Tooltip>
</Box>
);
};
export default NotificationBadgeGroup;
This approach gives users more clarity about the specific types of notifications they have, allowing them to choose which ones to view.
Creating an Animated Notification Badge
For important notifications, adding animation can draw users' attention:
// AnimatedNotificationBadge.jsx
import React, { useState, useEffect } from 'react';
import { Badge, keyframes } from '@mui/material';
import { styled } from '@mui/material/styles';
// Define animations
const pulse = keyframes`
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
`;
const bounce = keyframes`
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-5px);
}
60% {
transform: translateY(-2px);
}
`;
// Create styled versions of Badge
const PulseBadge = styled(Badge)(({ theme }) => ({
'& .MuiBadge-badge': {
animation: `${pulse} 1.5s infinite`,
},
}));
const BounceBadge = styled(Badge)(({ theme }) => ({
'& .MuiBadge-badge': {
animation: `${bounce} 1.5s infinite`,
},
}));
const AnimatedNotificationBadge = ({
count = 0,
animation = 'pulse',
animationDuration = 5000,
color = 'error',
children,
...props
}) => {
const [isAnimating, setIsAnimating] = useState(count > 0);
// Control animation duration
useEffect(() => {
if (count > 0) {
setIsAnimating(true);
if (animationDuration !== Infinity) {
const timer = setTimeout(() => {
setIsAnimating(false);
}, animationDuration);
return () => clearTimeout(timer);
}
} else {
setIsAnimating(false);
}
}, [count, animationDuration]);
// Choose the appropriate badge based on animation type
const BadgeComponent = isAnimating
? (animation === 'bounce' ? BounceBadge : PulseBadge)
: Badge;
return (
<BadgeComponent
badgeContent={count}
color={color}
invisible={count === 0}
{...props} >
{children}
</BadgeComponent>
);
};
export default AnimatedNotificationBadge;
This component provides different animation options for badges and automatically stops the animation after a specified duration.
Best Practices and Common Issues
Let's cover some best practices and common issues when working with notification badges.
Performance Considerations
Badges can impact performance if not used carefully:
-
Limit the number of animated badges on screen at once. Animations can be CPU-intensive, especially on mobile devices.
-
Use the
invisible
prop efficiently. When a badge has no content to display, make sure it's set to invisible to avoid unnecessary rendering. -
Debounce notification updates for frequently changing values:
import { useState, useEffect } from 'react';
import { debounce } from 'lodash';
const useDebounceNotificationCount = (count, delay = 300) => {
const [debouncedCount, setDebouncedCount] = useState(count);
useEffect(() => {
const handler = debounce(() => {
setDebouncedCount(count);
}, delay);
handler();
return () => {
handler.cancel();
};
}, [count, delay]);
return debouncedCount;
};
- Use memoization for complex badge components to prevent unnecessary re-renders:
import React, { memo } from 'react';
import { Badge } from '@mui/material';
const OptimizedBadge = memo(({ count, children, ...props }) => {
return (
<Badge
badgeContent={count}
invisible={count === 0}
{...props} >
{children}
</Badge>
);
});
export default OptimizedBadge;
Common Issues and Solutions
Issue 1: Badge Position Shifting
Problem: Badge position shifts when the count changes from single to double digits.
Solution: Set a minimum width for the badge:
<Badge
badgeContent={count}
sx={{
'& .MuiBadge-badge': {
minWidth: '22px', // Ensures consistent width
height: '22px',
padding: 0,
},
}}
>
<NotificationsIcon />
</Badge>
Issue 2: Badge Overlapping with Parent Content
Problem: Badge overlaps with the content it's attached to.
Solution: Adjust the overlap and position:
<Badge
badgeContent={count}
overlap="circular" // Use 'rectangular' for non-circular elements
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
sx={{
'& .MuiBadge-badge': {
right: -3,
top: 3,
border: '2px solid white', // Creates separation
},
}}
>
<Avatar alt="User" src="/user-avatar.jpg" />
</Badge>
Issue 3: Badge Not Visible on Dark Backgrounds
Problem: Badge is difficult to see on dark backgrounds.
Solution: Add a border to create contrast:
<Badge
badgeContent={count}
color="error"
sx={{
'& .MuiBadge-badge': {
border: '2px solid #121212', // Match your dark background
},
}}
>
<NotificationsIcon />
</Badge>
Issue 4: Inconsistent Badge Appearance Across Browsers
Problem: Badges look different in different browsers.
Solution: Use more explicit styling:
<Badge
badgeContent={count}
sx={{
'& .MuiBadge-badge': {
fontFamily: 'inherit',
fontSize: '0.75rem',
fontWeight: 'bold',
lineHeight: 1,
textAlign: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
}}
>
<NotificationsIcon />
</Badge>
Accessibility Best Practices
- Always provide accessible labels for badges:
<IconButton aria-label={`${count} unread messages`}>
<Badge badgeContent={count} color="primary">
<MailIcon />
</Badge>
</IconButton>
- Ensure sufficient color contrast between the badge and its background:
<Badge
badgeContent={count}
sx={{
'& .MuiBadge-badge': {
backgroundColor: '#D32F2F', // Ensure this has 4.5:1 contrast with background
color: '#FFFFFF',
},
}}
>
<NotificationsIcon />
</Badge>
- Don't rely solely on color to convey information - use text, numbers, or icons in addition to color:
// Bad: Only uses color to indicate status
<Badge variant="dot" color="success" />
// Good: Uses both color and text
<Badge badgeContent="Online" color="success" />
- Ensure badges are keyboard navigable when they're interactive:
<Badge
badgeContent={count}
component="button"
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}}
tabIndex={0}
aria-label={`${count} notifications`}
>
<NotificationsIcon />
</Badge>
Wrapping Up
In this comprehensive guide, we've explored how to use MUI's Badge component to create a professional notification system for messaging UIs. We've covered everything from basic implementation to advanced customization, real-time updates, and accessibility considerations.
The Badge component is a powerful tool in your UI arsenal, allowing you to communicate important information to users in a compact, visually appealing way. By following the patterns and practices outlined in this guide, you can create notification indicators that are both functional and delightful to use.
Remember that effective notification systems balance visibility with unobtrusiveness - they should catch the user's attention when needed without becoming distracting. With MUI's Badge component and the techniques we've explored, you now have all the tools you need to strike that perfect balance in your applications.