Menu

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:

PropTypeDefaultDescription
anchorOrigin vertical: 'top' | 'bottom', horizontal: 'left' | 'right' vertical: 'top', horizontal: 'right'

Position where the badge should appear relative to the wrapped element

badgeContentnode-The content rendered within the badge
childrennode-The element that the badge will be added to
color

'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' | string

'default'The color of the badge
componentelementType'span'The component used for the root node
invisibleboolfalseControls whether the badge is visible
maxnumber99Max count to show
overlap'circular' | 'rectangular''rectangular'How the badge should overlap its children
showZeroboolfalseControls the visibility when badgeContent is zero
variant'dot' | 'standard' | string'standard'The variant to use
sxobject-

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:

  1. Standard: Displays a numerical value or text (default)
  2. 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:

  1. A dot badge indicating online status on the user's avatar
  2. 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:

  1. Provides proper ARIA labels for screen readers
  2. Includes visually hidden text for more descriptive announcements
  3. Adds optional tooltips for additional context
  4. 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:

  1. Using proper ARIA attributes
  2. Supporting keyboard activation (Enter/Space)
  3. Managing focus correctly when opening/closing the menu
  4. 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:

  1. Limit the number of animated badges on screen at once. Animations can be CPU-intensive, especially on mobile devices.

  2. Use the invisible prop efficiently. When a badge has no content to display, make sure it's set to invisible to avoid unnecessary rendering.

  3. 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;
};
  1. 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

  1. Always provide accessible labels for badges:

<IconButton aria-label={`${count} unread messages`}>
  <Badge badgeContent={count} color="primary">
    <MailIcon />
  </Badge>
</IconButton>
  1. 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>
  1. 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" />
  1. 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.