Claude Code Plugins

Community-maintained marketplace

Feedback

Renders notification bell icon with unread count badge, pulse animations for new notifications, and dropdown panel (320px × max 400px) displaying notification items with purple theme for VERY IMPORTANT tasks. Includes mark-as-read interaction and empty state handling.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name notification-ui
description Renders notification bell icon with unread count badge, pulse animations for new notifications, and dropdown panel (320px × max 400px) displaying notification items with purple theme for VERY IMPORTANT tasks. Includes mark-as-read interaction and empty state handling.

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)
function BellIcon({ unreadCount, onClick }) {
  return (
    <button
      className="bell-icon-button"
      onClick={onClick}
      aria-label={`Notifications (${unreadCount} unread)`}
    >
      <svg
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
        className="bell-icon"
      >
        <path
          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"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      </svg>
      {unreadCount > 0 && (
        <span className="badge">{unreadCount > 99 ? '99+' : unreadCount}</span>
      )}
    </button>
  );
}

Bell Icon Styling

.bell-icon-button {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
  background: transparent;
  border: none;
  cursor: pointer;
  border-radius: 6px;
  transition: background-color 0.2s ease;
}

.bell-icon-button:hover {
  background-color: #F3F4F6;
}

.bell-icon {
  color: #374151;
  transition: color 0.2s ease;
}

.bell-icon-button:hover .bell-icon {
  color: #1F2937;
}

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
.badge {
  position: absolute;
  top: 4px;
  right: 4px;
  min-width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #DC2626; /* Red */
  color: #FFFFFF; /* White */
  font-size: 10px;
  font-weight: 700;
  border-radius: 50%;
  padding: 0 4px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}

Badge Display Logic

function getBadgeText(count) {
  if (count === 0) return null; // No badge
  if (count > 99) return '99+';
  return count.toString();
}

Pulse Animation

Animation Specifications

  • Duration: 2 seconds
  • Effect: Purple glow (#8B5CF6)
  • Trigger: On new notification arrival
  • Iterations: 3 times, then stop
@keyframes pulse {
  0%, 100% {
    box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.7);
  }
  50% {
    box-shadow: 0 0 0 8px rgba(139, 92, 246, 0);
  }
}

.bell-icon-button.pulsing {
  animation: pulse 2s ease-in-out 3;
}

Animation Trigger

function NotificationBell({ notifications }) {
  const [isPulsing, setIsPulsing] = useState(false);
  const prevCountRef = useRef(notifications.length);

  useEffect(() => {
    // Detect new notification
    if (notifications.length > prevCountRef.current) {
      setIsPulsing(true);

      // Stop animation after 6 seconds (3 iterations × 2s)
      setTimeout(() => setIsPulsing(false), 6000);
    }

    prevCountRef.current = notifications.length;
  }, [notifications.length]);

  return (
    <BellIcon
      className={isPulsing ? 'pulsing' : ''}
      unreadCount={notifications.filter(n => !n.read).length}
    />
  );
}

Notification Dropdown Panel

Panel Specifications

  • Width: 320px
  • Max Height: 400px
  • Scrollable: Yes (overflow-y: auto)
  • Position: Dropdown below bell icon
  • Elevation: Shadow for depth
.notification-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: 320px;
  max-height: 400px;
  overflow-y: auto;
  background-color: #FFFFFF;
  border: 1px solid #E5E7EB;
  border-radius: 8px;
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
              0 4px 6px -2px rgba(0, 0, 0, 0.05);
  z-index: 1000;
}

.notification-dropdown-header {
  padding: 16px;
  border-bottom: 1px solid #E5E7EB;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.notification-dropdown-title {
  font-size: 16px;
  font-weight: 600;
  color: #111827;
}

.mark-all-read {
  font-size: 12px;
  color: #8B5CF6;
  background: none;
  border: none;
  cursor: pointer;
  font-weight: 600;
}

.mark-all-read:hover {
  color: #7C3AED;
  text-decoration: underline;
}

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
.notification-item {
  padding: 16px;
  border-bottom: 1px solid #F3F4F6;
  cursor: pointer;
  transition: background-color 0.15s ease;
}

.notification-item:hover {
  background-color: #F9FAFB;
}

.notification-item:last-child {
  border-bottom: none;
}

.notification-item.unread {
  background-color: #FAF5FF; /* Very light purple */
}

.notification-item.unread:hover {
  background-color: #F3E8FF; /* Light purple */
}

.notification-content {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.notification-header {
  display: flex;
  align-items: flex-start;
  gap: 8px;
}

.notification-priority-indicator {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background-color: #8B5CF6; /* Purple for VERY IMPORTANT */
  margin-top: 6px;
  flex-shrink: 0;
}

.notification-message {
  font-size: 14px;
  color: #374151;
  line-height: 1.5;
  flex: 1;
}

.notification-item.unread .notification-message {
  font-weight: 600;
  color: #111827;
}

.notification-time {
  font-size: 12px;
  color: #9CA3AF;
  display: flex;
  align-items: center;
  gap: 4px;
}

.unread-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background-color: #8B5CF6; /* Purple */
}

Complete Notification Item Component

function NotificationItem({ notification, onMarkAsRead }) {
  const handleClick = () => {
    if (!notification.read) {
      onMarkAsRead(notification.id);
    }
  };

  const relativeTime = formatRelativeTime(notification.timestamp);

  return (
    <div
      className={`notification-item ${notification.read ? '' : 'unread'}`}
      onClick={handleClick}
    >
      <div className="notification-content">
        <div className="notification-header">
          <div className="notification-priority-indicator" />
          <p className="notification-message">{notification.message}</p>
        </div>
        <div className="notification-time">
          {!notification.read && <span className="unread-dot" />}
          <span>{relativeTime}</span>
        </div>
      </div>
    </div>
  );
}

function formatRelativeTime(timestamp) {
  const now = Date.now();
  const diff = now - timestamp;
  const minutes = Math.floor(diff / (1000 * 60));
  const hours = Math.floor(diff / (1000 * 60 * 60));
  const days = Math.floor(diff / (1000 * 60 * 60 * 24));

  if (minutes < 1) return 'Just now';
  if (minutes < 60) return `${minutes}m ago`;
  if (hours < 24) return `${hours}h ago`;
  return `${days}d ago`;
}

Mark-as-Read Interaction

Click to Mark Read

Clicking an unread notification marks it as read:

function handleNotificationClick(notification) {
  if (!notification.read) {
    // Mark as read
    notificationPersistence.markAsRead(notification.id);

    // Update UI state
    setNotifications(notificationPersistence.getAll());
    setUnreadCount(notificationPersistence.getUnreadCount());

    // Optional: Navigate to task
    navigateToTask(notification.taskId);
  }
}

Visual Feedback

function NotificationItem({ notification, onMarkAsRead }) {
  const [isMarking, setIsMarking] = useState(false);

  const handleClick = async () => {
    if (!notification.read && !isMarking) {
      setIsMarking(true);
      await onMarkAsRead(notification.id);
      setIsMarking(false);
    }
  };

  return (
    <div
      className={`notification-item ${notification.read ? '' : 'unread'} ${isMarking ? 'marking' : ''}`}
      onClick={handleClick}
    >
      {/* ... */}
    </div>
  );
}

Empty State

Empty State Component

function EmptyState() {
  return (
    <div className="notification-empty-state">
      <div className="empty-icon">🔔</div>
      <p className="empty-title">No notifications</p>
      <p className="empty-description">
        You're all caught up! Notifications for VERY IMPORTANT tasks will appear here.
      </p>
    </div>
  );
}

Empty State Styling

.notification-empty-state {
  padding: 48px 24px;
  text-align: center;
  color: #6B7280;
}

.empty-icon {
  font-size: 48px;
  margin-bottom: 16px;
  opacity: 0.4;
}

.empty-title {
  font-size: 16px;
  font-weight: 600;
  color: #374151;
  margin-bottom: 8px;
}

.empty-description {
  font-size: 14px;
  color: #9CA3AF;
  line-height: 1.5;
}

Complete Notification System

function NotificationSystem() {
  const [isOpen, setIsOpen] = useState(false);
  const [notifications, setNotifications] = useState([]);
  const [unreadCount, setUnreadCount] = useState(0);
  const dropdownRef = useRef(null);

  // Load notifications
  useEffect(() => {
    const loaded = notificationPersistence.getAll();
    setNotifications(loaded);
    setUnreadCount(notificationPersistence.getUnreadCount());
  }, []);

  // Close dropdown when clicking outside
  useEffect(() => {
    function handleClickOutside(event) {
      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
        setIsOpen(false);
      }
    }

    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  const handleMarkAsRead = (id) => {
    notificationPersistence.markAsRead(id);
    setNotifications(notificationPersistence.getAll());
    setUnreadCount(notificationPersistence.getUnreadCount());
  };

  const handleMarkAllAsRead = () => {
    notificationPersistence.markAllAsRead();
    setNotifications(notificationPersistence.getAll());
    setUnreadCount(0);
  };

  return (
    <div className="notification-system" ref={dropdownRef}>
      <NotificationBell
        unreadCount={unreadCount}
        onClick={() => setIsOpen(!isOpen)}
        notifications={notifications}
      />

      {isOpen && (
        <div className="notification-dropdown">
          <div className="notification-dropdown-header">
            <h3 className="notification-dropdown-title">Notifications</h3>
            {unreadCount > 0 && (
              <button className="mark-all-read" onClick={handleMarkAllAsRead}>
                Mark all as read
              </button>
            )}
          </div>

          {notifications.length === 0 ? (
            <EmptyState />
          ) : (
            <div className="notification-list">
              {notifications.map(notification => (
                <NotificationItem
                  key={notification.id}
                  notification={notification}
                  onMarkAsRead={handleMarkAsRead}
                />
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

Responsive Behavior

Mobile Adjustments

@media (max-width: 640px) {
  .notification-dropdown {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    max-height: 100%;
    border-radius: 0;
  }

  .notification-dropdown-header {
    position: sticky;
    top: 0;
    background-color: #FFFFFF;
    z-index: 1;
  }
}

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)

Performance Considerations

  • 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