Notification UI Skill
Overview
The notification UI skill provides the visual interface for displaying notifications, including a bell icon with badge, animated indicators, and an interactive notification panel.
When to Apply
Apply this skill:
- When rendering the app header (bell icon)
- When displaying unread notification count
- When showing notification panel/dropdown
- When animating new notification arrivals
- When user interacts with notifications (click to mark as read)
- When displaying empty state (no notifications)
Bell Icon Specifications
Icon Size and Position
- Size: 24px × 24px
- Position: Top-right header
- Color: Default #374151 (gray), hover #1F2937 (dark gray)
jsx
1function BellIcon({ unreadCount, onClick }) {
2 return (
3 <button
4 className="bell-icon-button"
5 onClick={onClick}
6 aria-label={`Notifications (${unreadCount} unread)`}
7 >
8 <svg
9 width="24"
10 height="24"
11 viewBox="0 0 24 24"
12 fill="none"
13 xmlns="http://www.w3.org/2000/svg"
14 className="bell-icon"
15 >
16 <path
17 d="M15 17H20L18.5951 15.5951C18.2141 15.2141 18 14.6973 18 14.1585V11C18 8.38757 16.3304 6.16509 14 5.34142V5C14 3.89543 13.1046 3 12 3C10.8954 3 10 3.89543 10 5V5.34142C7.66962 6.16509 6 8.38757 6 11V14.1585C6 14.6973 5.78595 15.2141 5.40493 15.5951L4 17H9M15 17V18C15 19.6569 13.6569 21 12 21C10.3431 21 9 19.6569 9 18V17M15 17H9"
18 stroke="currentColor"
19 strokeWidth="2"
20 strokeLinecap="round"
21 strokeLinejoin="round"
22 />
23 </svg>
24 {unreadCount > 0 && (
25 <span className="badge">{unreadCount > 99 ? '99+' : unreadCount}</span>
26 )}
27 </button>
28 );
29}
Bell Icon Styling
css
1.bell-icon-button {
2 position: relative;
3 display: flex;
4 align-items: center;
5 justify-content: center;
6 padding: 8px;
7 background: transparent;
8 border: none;
9 cursor: pointer;
10 border-radius: 6px;
11 transition: background-color 0.2s ease;
12}
13
14.bell-icon-button:hover {
15 background-color: #F3F4F6;
16}
17
18.bell-icon {
19 color: #374151;
20 transition: color 0.2s ease;
21}
22
23.bell-icon-button:hover .bell-icon {
24 color: #1F2937;
25}
Unread Count Badge
Badge Specifications
- Shape: Circular
- Background: #DC2626 (red)
- Text: White (#FFFFFF)
- Display: Count up to 99, then "99+"
- Position: Top-right corner of bell icon
css
1.badge {
2 position: absolute;
3 top: 4px;
4 right: 4px;
5 min-width: 18px;
6 height: 18px;
7 display: flex;
8 align-items: center;
9 justify-content: center;
10 background-color: #DC2626; /* Red */
11 color: #FFFFFF; /* White */
12 font-size: 10px;
13 font-weight: 700;
14 border-radius: 50%;
15 padding: 0 4px;
16 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
17}
Badge Display Logic
javascript
1function getBadgeText(count) {
2 if (count === 0) return null; // No badge
3 if (count > 99) return '99+';
4 return count.toString();
5}
Pulse Animation
Animation Specifications
- Duration: 2 seconds
- Effect: Purple glow (#8B5CF6)
- Trigger: On new notification arrival
- Iterations: 3 times, then stop
css
1@keyframes pulse {
2 0%, 100% {
3 box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.7);
4 }
5 50% {
6 box-shadow: 0 0 0 8px rgba(139, 92, 246, 0);
7 }
8}
9
10.bell-icon-button.pulsing {
11 animation: pulse 2s ease-in-out 3;
12}
Animation Trigger
jsx
1function NotificationBell({ notifications }) {
2 const [isPulsing, setIsPulsing] = useState(false);
3 const prevCountRef = useRef(notifications.length);
4
5 useEffect(() => {
6 // Detect new notification
7 if (notifications.length > prevCountRef.current) {
8 setIsPulsing(true);
9
10 // Stop animation after 6 seconds (3 iterations × 2s)
11 setTimeout(() => setIsPulsing(false), 6000);
12 }
13
14 prevCountRef.current = notifications.length;
15 }, [notifications.length]);
16
17 return (
18 <BellIcon
19 className={isPulsing ? 'pulsing' : ''}
20 unreadCount={notifications.filter(n => !n.read).length}
21 />
22 );
23}
Notification Dropdown Panel
Panel Specifications
- Width: 320px
- Max Height: 400px
- Scrollable: Yes (overflow-y: auto)
- Position: Dropdown below bell icon
- Elevation: Shadow for depth
css
1.notification-dropdown {
2 position: absolute;
3 top: calc(100% + 8px);
4 right: 0;
5 width: 320px;
6 max-height: 400px;
7 overflow-y: auto;
8 background-color: #FFFFFF;
9 border: 1px solid #E5E7EB;
10 border-radius: 8px;
11 box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
12 0 4px 6px -2px rgba(0, 0, 0, 0.05);
13 z-index: 1000;
14}
15
16.notification-dropdown-header {
17 padding: 16px;
18 border-bottom: 1px solid #E5E7EB;
19 display: flex;
20 justify-content: space-between;
21 align-items: center;
22}
23
24.notification-dropdown-title {
25 font-size: 16px;
26 font-weight: 600;
27 color: #111827;
28}
29
30.mark-all-read {
31 font-size: 12px;
32 color: #8B5CF6;
33 background: none;
34 border: none;
35 cursor: pointer;
36 font-weight: 600;
37}
38
39.mark-all-read:hover {
40 color: #7C3AED;
41 text-decoration: underline;
42}
Notification Item Styling
Item Specifications
- Padding: 16px
- Border: Bottom separator (1px solid #F3F4F6)
- Hover: Background change to #F9FAFB
- Purple Theme: VERY IMPORTANT notifications use #8B5CF6 accent
css
1.notification-item {
2 padding: 16px;
3 border-bottom: 1px solid #F3F4F6;
4 cursor: pointer;
5 transition: background-color 0.15s ease;
6}
7
8.notification-item:hover {
9 background-color: #F9FAFB;
10}
11
12.notification-item:last-child {
13 border-bottom: none;
14}
15
16.notification-item.unread {
17 background-color: #FAF5FF; /* Very light purple */
18}
19
20.notification-item.unread:hover {
21 background-color: #F3E8FF; /* Light purple */
22}
23
24.notification-content {
25 display: flex;
26 flex-direction: column;
27 gap: 6px;
28}
29
30.notification-header {
31 display: flex;
32 align-items: flex-start;
33 gap: 8px;
34}
35
36.notification-priority-indicator {
37 width: 6px;
38 height: 6px;
39 border-radius: 50%;
40 background-color: #8B5CF6; /* Purple for VERY IMPORTANT */
41 margin-top: 6px;
42 flex-shrink: 0;
43}
44
45.notification-message {
46 font-size: 14px;
47 color: #374151;
48 line-height: 1.5;
49 flex: 1;
50}
51
52.notification-item.unread .notification-message {
53 font-weight: 600;
54 color: #111827;
55}
56
57.notification-time {
58 font-size: 12px;
59 color: #9CA3AF;
60 display: flex;
61 align-items: center;
62 gap: 4px;
63}
64
65.unread-dot {
66 width: 8px;
67 height: 8px;
68 border-radius: 50%;
69 background-color: #8B5CF6; /* Purple */
70}
Complete Notification Item Component
jsx
1function NotificationItem({ notification, onMarkAsRead }) {
2 const handleClick = () => {
3 if (!notification.read) {
4 onMarkAsRead(notification.id);
5 }
6 };
7
8 const relativeTime = formatRelativeTime(notification.timestamp);
9
10 return (
11 <div
12 className={`notification-item ${notification.read ? '' : 'unread'}`}
13 onClick={handleClick}
14 >
15 <div className="notification-content">
16 <div className="notification-header">
17 <div className="notification-priority-indicator" />
18 <p className="notification-message">{notification.message}</p>
19 </div>
20 <div className="notification-time">
21 {!notification.read && <span className="unread-dot" />}
22 <span>{relativeTime}</span>
23 </div>
24 </div>
25 </div>
26 );
27}
28
29function formatRelativeTime(timestamp) {
30 const now = Date.now();
31 const diff = now - timestamp;
32 const minutes = Math.floor(diff / (1000 * 60));
33 const hours = Math.floor(diff / (1000 * 60 * 60));
34 const days = Math.floor(diff / (1000 * 60 * 60 * 24));
35
36 if (minutes < 1) return 'Just now';
37 if (minutes < 60) return `${minutes}m ago`;
38 if (hours < 24) return `${hours}h ago`;
39 return `${days}d ago`;
40}
Mark-as-Read Interaction
Click to Mark Read
Clicking an unread notification marks it as read:
jsx
1function handleNotificationClick(notification) {
2 if (!notification.read) {
3 // Mark as read
4 notificationPersistence.markAsRead(notification.id);
5
6 // Update UI state
7 setNotifications(notificationPersistence.getAll());
8 setUnreadCount(notificationPersistence.getUnreadCount());
9
10 // Optional: Navigate to task
11 navigateToTask(notification.taskId);
12 }
13}
Visual Feedback
jsx
1function NotificationItem({ notification, onMarkAsRead }) {
2 const [isMarking, setIsMarking] = useState(false);
3
4 const handleClick = async () => {
5 if (!notification.read && !isMarking) {
6 setIsMarking(true);
7 await onMarkAsRead(notification.id);
8 setIsMarking(false);
9 }
10 };
11
12 return (
13 <div
14 className={`notification-item ${notification.read ? '' : 'unread'} ${isMarking ? 'marking' : ''}`}
15 onClick={handleClick}
16 >
17 {/* ... */}
18 </div>
19 );
20}
Empty State
Empty State Component
jsx
1function EmptyState() {
2 return (
3 <div className="notification-empty-state">
4 <div className="empty-icon">🔔</div>
5 <p className="empty-title">No notifications</p>
6 <p className="empty-description">
7 You're all caught up! Notifications for VERY IMPORTANT tasks will appear here.
8 </p>
9 </div>
10 );
11}
Empty State Styling
css
1.notification-empty-state {
2 padding: 48px 24px;
3 text-align: center;
4 color: #6B7280;
5}
6
7.empty-icon {
8 font-size: 48px;
9 margin-bottom: 16px;
10 opacity: 0.4;
11}
12
13.empty-title {
14 font-size: 16px;
15 font-weight: 600;
16 color: #374151;
17 margin-bottom: 8px;
18}
19
20.empty-description {
21 font-size: 14px;
22 color: #9CA3AF;
23 line-height: 1.5;
24}
Complete Notification System
jsx
1function NotificationSystem() {
2 const [isOpen, setIsOpen] = useState(false);
3 const [notifications, setNotifications] = useState([]);
4 const [unreadCount, setUnreadCount] = useState(0);
5 const dropdownRef = useRef(null);
6
7 // Load notifications
8 useEffect(() => {
9 const loaded = notificationPersistence.getAll();
10 setNotifications(loaded);
11 setUnreadCount(notificationPersistence.getUnreadCount());
12 }, []);
13
14 // Close dropdown when clicking outside
15 useEffect(() => {
16 function handleClickOutside(event) {
17 if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
18 setIsOpen(false);
19 }
20 }
21
22 document.addEventListener('mousedown', handleClickOutside);
23 return () => document.removeEventListener('mousedown', handleClickOutside);
24 }, []);
25
26 const handleMarkAsRead = (id) => {
27 notificationPersistence.markAsRead(id);
28 setNotifications(notificationPersistence.getAll());
29 setUnreadCount(notificationPersistence.getUnreadCount());
30 };
31
32 const handleMarkAllAsRead = () => {
33 notificationPersistence.markAllAsRead();
34 setNotifications(notificationPersistence.getAll());
35 setUnreadCount(0);
36 };
37
38 return (
39 <div className="notification-system" ref={dropdownRef}>
40 <NotificationBell
41 unreadCount={unreadCount}
42 onClick={() => setIsOpen(!isOpen)}
43 notifications={notifications}
44 />
45
46 {isOpen && (
47 <div className="notification-dropdown">
48 <div className="notification-dropdown-header">
49 <h3 className="notification-dropdown-title">Notifications</h3>
50 {unreadCount > 0 && (
51 <button className="mark-all-read" onClick={handleMarkAllAsRead}>
52 Mark all as read
53 </button>
54 )}
55 </div>
56
57 {notifications.length === 0 ? (
58 <EmptyState />
59 ) : (
60 <div className="notification-list">
61 {notifications.map(notification => (
62 <NotificationItem
63 key={notification.id}
64 notification={notification}
65 onMarkAsRead={handleMarkAsRead}
66 />
67 ))}
68 </div>
69 )}
70 </div>
71 )}
72 </div>
73 );
74}
Responsive Behavior
Mobile Adjustments
css
1@media (max-width: 640px) {
2 .notification-dropdown {
3 position: fixed;
4 top: 0;
5 left: 0;
6 right: 0;
7 bottom: 0;
8 width: 100%;
9 max-height: 100%;
10 border-radius: 0;
11 }
12
13 .notification-dropdown-header {
14 position: sticky;
15 top: 0;
16 background-color: #FFFFFF;
17 z-index: 1;
18 }
19}
Integration Points
This skill integrates with:
- Notification Persistence Skill: Reads notification data
- Notification Trigger Skill: Displays triggered notifications
- Priority Classification Skill: Uses purple theme for VERY IMPORTANT
- Notification Experience Agent: Coordinates UI interactions
Accessibility
- Bell icon button has aria-label with unread count
- Keyboard navigation supported (Enter/Space to open/close)
- Focus management in dropdown
- Screen reader announcements for new notifications
- Sufficient color contrast ratios (WCAG AA)
- Dropdown renders only when open (conditional rendering)
- Virtual scrolling for large notification lists (>50)
- Debounced mark-as-read operations
- Memoized components to prevent unnecessary re-renders