UI Components Medium
Notification Center
Dropdown notification panel with unread badge, mark-all-read, grouped notifications, and infinite-scroll loading. No libraries.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f9fafb;
color: #111;
}
.demo-page {
min-height: 100vh;
}
.demo-header {
display: flex;
align-items: center;
gap: 32px;
padding: 16px 32px;
background: #fff;
border-bottom: 1px solid #e5e7eb;
position: sticky;
top: 0;
z-index: 20;
}
.demo-logo {
font-size: 18px;
font-weight: 800;
color: #111;
}
.demo-nav {
display: flex;
gap: 20px;
flex: 1;
}
.demo-nav a {
font-size: 14px;
color: #6b7280;
text-decoration: none;
}
.header-actions {
position: relative;
}
/* Bell button */
.bell-btn {
position: relative;
width: 40px;
height: 40px;
border: none;
background: #f3f4f6;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #374151;
transition: background 0.15s;
}
.bell-btn:hover {
background: #e5e7eb;
}
.bell-btn.active {
background: #eef2ff;
color: #6366f1;
}
.bell-badge {
position: absolute;
top: -4px;
right: -4px;
min-width: 18px;
height: 18px;
background: #ef4444;
color: #fff;
font-size: 11px;
font-weight: 700;
border-radius: 10px;
padding: 0 4px;
line-height: 18px;
text-align: center;
border: 2px solid #fff;
transition: transform 0.2s;
}
.bell-badge.hidden {
display: none;
}
/* Panel */
.notif-panel {
position: fixed;
top: 64px;
right: 24px;
width: 360px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
z-index: 30;
overflow: hidden;
animation: slideDown 0.2s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.notif-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f3f4f6;
}
.notif-title {
font-size: 15px;
font-weight: 700;
color: #111;
}
.mark-all-btn {
background: none;
border: none;
font-size: 13px;
color: #6366f1;
cursor: pointer;
font-weight: 600;
padding: 4px 8px;
border-radius: 6px;
transition: background 0.15s;
}
.mark-all-btn:hover {
background: #eef2ff;
}
.notif-list {
max-height: 400px;
overflow-y: auto;
}
.notif-group-label {
padding: 10px 20px 6px;
font-size: 11px;
font-weight: 700;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.notif-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 20px;
cursor: pointer;
transition: background 0.12s;
position: relative;
}
.notif-item:hover {
background: #f9fafb;
}
.notif-item.unread {
background: #fafbff;
}
.notif-item.unread:hover {
background: #f0f4ff;
}
.notif-avatar {
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.notif-body {
flex: 1;
min-width: 0;
}
.notif-text {
font-size: 13px;
color: #374151;
line-height: 1.5;
margin: 0 0 3px;
}
.notif-time {
font-size: 12px;
color: #9ca3af;
margin: 0;
}
.unread-dot {
width: 8px;
height: 8px;
background: #6366f1;
border-radius: 50%;
flex-shrink: 0;
margin-top: 4px;
}
.notif-footer {
padding: 12px 20px;
border-top: 1px solid #f3f4f6;
text-align: center;
}
.load-more-btn {
background: none;
border: none;
font-size: 13px;
color: #6366f1;
cursor: pointer;
font-weight: 600;
padding: 6px 16px;
border-radius: 8px;
transition: background 0.15s;
}
.load-more-btn:hover {
background: #eef2ff;
}
/* Backdrop */
.notif-backdrop {
position: fixed;
inset: 0;
z-index: 15;
}
.main-content {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - 64px);
}
.demo-hint {
font-size: 15px;
color: #9ca3af;
}const bellBtn = document.getElementById("bellBtn");
const bellBadge = document.getElementById("bellBadge");
const notifPanel = document.getElementById("notifPanel");
const notifBackdrop = document.getElementById("notifBackdrop");
const markAllBtn = document.getElementById("markAllBtn");
const loadMoreBtn = document.getElementById("loadMoreBtn");
let unreadCount = document.querySelectorAll(".notif-item.unread").length;
function updateBadge() {
if (unreadCount > 0) {
bellBadge.textContent = unreadCount;
bellBadge.classList.remove("hidden");
} else {
bellBadge.classList.add("hidden");
}
}
function openPanel() {
notifPanel.hidden = false;
notifBackdrop.hidden = false;
bellBtn.classList.add("active");
}
function closePanel() {
notifPanel.hidden = true;
notifBackdrop.hidden = true;
bellBtn.classList.remove("active");
}
bellBtn.addEventListener("click", (e) => {
e.stopPropagation();
notifPanel.hidden ? openPanel() : closePanel();
});
notifBackdrop.addEventListener("click", closePanel);
// Mark individual as read on click
document.getElementById("notifList").addEventListener("click", (e) => {
const item = e.target.closest(".notif-item");
if (!item) return;
if (item.classList.contains("unread")) {
item.classList.remove("unread");
const dot = item.querySelector(".unread-dot");
if (dot) dot.remove();
unreadCount = Math.max(0, unreadCount - 1);
updateBadge();
}
});
// Mark all read
markAllBtn.addEventListener("click", () => {
document.querySelectorAll(".notif-item.unread").forEach((item) => {
item.classList.remove("unread");
const dot = item.querySelector(".unread-dot");
if (dot) dot.remove();
});
unreadCount = 0;
updateBadge();
});
// Load more (demo: append a fake item)
let loadCount = 0;
loadMoreBtn.addEventListener("click", () => {
loadCount++;
const list = document.getElementById("notifList");
const item = document.createElement("div");
item.className = "notif-item";
item.innerHTML = `
<div class="notif-avatar" style="background:#f3f4f6;color:#6b7280;">📋</div>
<div class="notif-body">
<p class="notif-text">Older notification #${loadCount}</p>
<p class="notif-time">${loadCount + 3} days ago</p>
</div>
`;
list.appendChild(item);
if (loadCount >= 3) loadMoreBtn.textContent = "No more notifications";
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Notification Center</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo-page">
<header class="demo-header">
<span class="demo-logo">Acme</span>
<nav class="demo-nav">
<a href="#">Dashboard</a>
<a href="#">Projects</a>
</nav>
<div class="header-actions">
<button class="bell-btn" id="bellBtn" aria-label="Notifications">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
</svg>
<span class="bell-badge" id="bellBadge">3</span>
</button>
</div>
</header>
<div class="main-content">
<p class="demo-hint">Click the bell icon to open notifications</p>
</div>
<!-- Notification panel -->
<div class="notif-panel" id="notifPanel" hidden>
<div class="notif-header">
<span class="notif-title">Notifications</span>
<button class="mark-all-btn" id="markAllBtn">Mark all read</button>
</div>
<div class="notif-list" id="notifList">
<!-- Today group -->
<div class="notif-group-label">Today</div>
<div class="notif-item unread" data-id="1">
<div class="notif-avatar" style="background:#eef2ff;color:#6366f1;">💬</div>
<div class="notif-body">
<p class="notif-text"><strong>Alex Chen</strong> commented on <strong>Sprint #12</strong></p>
<p class="notif-time">5 min ago</p>
</div>
<div class="unread-dot"></div>
</div>
<div class="notif-item unread" data-id="2">
<div class="notif-avatar" style="background:#dcfce7;color:#16a34a;">✓</div>
<div class="notif-body">
<p class="notif-text"><strong>Deploy #84</strong> completed successfully</p>
<p class="notif-time">1 hr ago</p>
</div>
<div class="unread-dot"></div>
</div>
<div class="notif-item unread" data-id="3">
<div class="notif-avatar" style="background:#fef3c7;color:#d97706;">⚠️</div>
<div class="notif-body">
<p class="notif-text">Storage is at <strong>90%</strong> capacity</p>
<p class="notif-time">3 hr ago</p>
</div>
<div class="unread-dot"></div>
</div>
<!-- Earlier group -->
<div class="notif-group-label">Earlier</div>
<div class="notif-item" data-id="4">
<div class="notif-avatar" style="background:#f3e8ff;color:#9333ea;">👥</div>
<div class="notif-body">
<p class="notif-text"><strong>Maria Lopez</strong> joined your team</p>
<p class="notif-time">Yesterday</p>
</div>
</div>
<div class="notif-item" data-id="5">
<div class="notif-avatar" style="background:#fee2e2;color:#dc2626;">🚨</div>
<div class="notif-body">
<p class="notif-text">API rate limit warning on <strong>Production</strong></p>
<p class="notif-time">2 days ago</p>
</div>
</div>
</div>
<div class="notif-footer">
<button class="load-more-btn" id="loadMoreBtn">Load more</button>
</div>
</div>
<!-- Backdrop -->
<div class="notif-backdrop" id="notifBackdrop" hidden></div>
</div>
<script src="script.js"></script>
</body>
</html>Dropdown notification panel with bell icon + unread count badge, notification groups (Today / Earlier), mark-as-read on click, mark-all-read button, and a load-more trigger. Pure vanilla JS.