Notification Bell — Free HTML CSS JS Dropdown Snippet

Notification Bell · Dashboards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Absolute-positioned red unread badge with count number
toggle() adds/removes .open on the panel — one function for open and close
opacity + translateY(-8px) CSS transition on .panel.open
pointer-events: none when closed — invisible panel does not intercept clicks
.unread class drives all visual differences per notification item
markAll() removes .unread, removes .notif-dot, hides badge when count reaches zero
Click-outside close via e.target.closest(".bell-wrap") document listener
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons
Live split-pane editor — preview updates as you type

About this UI Snippet

Notification Bell — Unread Badge, Toggle Dropdown & Mark All Read

Screenshot of the Notification Bell snippet rendered live

A notification bell in the app header gives users a quick glance at pending alerts without navigating away from their current view — the full list typically lives in a notification center. The red unread badge communicates urgency; the dropdown reveals the notification list; Mark all read clears the badge. This is a complete, production-quality implementation with click-outside close.

The unread badge

The bell button has an absolutely positioned .badge div showing the unread count. It starts with a display: none state that can be shown conditionally. When there are unread notifications, the badge appears with a red background and white count number.

The dropdown toggle

toggle() calls panel.classList.toggle('open'). The .panel.open CSS rule transitions opacity from 0 to 1 and transform from translateY(-8px) to translateY(0). pointer-events: none when closed prevents the invisible panel from intercepting clicks. The dropdown has a fixed width and max-height with overflow-y: auto for long notification lists.

Unread notification styling

Notifications with .unread class have a blue .notif-dot indicator and a slightly tinted background. The .unread class drives all visual differences — removing it marks the notification as read.

Mark all read

markAll() queries all .notif.unread elements, removes the .unread class, removes the .notif-dot element, then hides the badge if the unread count reaches zero. This is the minimum viable implementation — in production, it would also fire an API call.

Click-outside close

A document.addEventListener('click', e => { if (!e.target.closest('.bell-wrap')) panel.classList.remove('open'); }) closes the panel when the user clicks outside. This is the same closest() pattern used in the Dropdown Menu snippet.

The badge counter

The unread badge uses position: absolute; top: -4px; right: -4px to sit above the bell icon. It shows a count number and uses min-width: 16px so it looks correct for single and double digit counts. When count exceeds 9, display "9+" instead of the full number. The badge disappears (display: none) when count reaches 0.

The dropdown open/close

Clicking the bell toggles .open on the dropdown panel, which uses scale(0.95) + opacity: 0 in the closed state and scale(1) + opacity: 1 in the open state. This CSS transition matches the popover pattern. The dropdown is positioned using position: absolute; right: 0; top: calc(100% + 8px). Click-outside detection uses document.addEventListener('click', e => { if (!e.target.closest('.bell-wrap')) close(); }).

Mark as read interaction

Each notification item has a "Mark read" button. Clicking it removes the .unread class from the notification and decrements the badge count. The mark-all-read button iterates all .unread items and marks them simultaneously. Both patterns update the badge count immediately — optimistic UI that syncs to the backend separately.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of tracing the dropdown's dismissal logic by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the document-level click listener's e.target.closest('.bell-wrap') check avoids immediately closing the panel on the same click that opened it, and why markAll() has to both remove the unread class and delete the notif-dot element rather than just one or the other. The same assistant can help optimize it, for instance asking whether toggling classList on every notif row during markAll() would still be fast with a very long notification list, or whether the panel's open/close transition should account for prefers-reduced-motion. It's also useful for extending the bell: ask it to add per-notification "mark as read" on click (not just mark-all), prepend newly arriving notifications from a WebSocket with their own entrance animation, or group notifications by day with sticky section headers. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "notification bell" dropdown in plain HTML, CSS, and JavaScript with an unread badge and click-outside dismissal — no dropdown library.

Requirements:
- A bell icon button with an absolutely positioned unread-count badge in its top-right corner, bordered to separate it from the button's background; the badge must be capable of being hidden entirely (not just showing "0").
- Clicking the bell toggles a dropdown panel open and closed using a single class toggle (not separate open/close functions), positioned below and right-aligned to the bell, that transitions in with an opacity and vertical-offset animation and must not intercept clicks while closed (its closed state must not be clickable even though the closed DOM technically still exists).
- A scrollable list of notification rows inside the panel, each with an avatar, message text, and a relative timestamp; unread rows must be visually distinguished from read ones (a tinted background plus a small colored dot) purely through one class name difference.
- A "Mark all read" action that removes the unread distinction (both the tint and the dot) from every currently-unread row in one action, and hides the bell's badge once no unread rows remain.
- The panel must close automatically when the user clicks anywhere outside the bell-and-panel wrapper, implemented with a single document-level click listener using closest() to detect clicks inside versus outside the component — this must not require a listener on every notification row.
- A footer link inside the panel for viewing the full notification history, visually separated from the list by a border.

Step by step

How to Use

  1. 1
    Click the bell iconClick the bell icon in the preview to open the notification panel. Click it again or click outside to close.
  2. 2
    Click "Mark all read"Click the Mark all read link to remove the unread styling from all notifications and hide the red badge.
  3. 3
    Update notification contentIn the HTML panel, update the .notif elements with real notification text, timestamps, and icons.
  4. 4
    Add new notificationsCopy a .notif div and paste it inside the notifications list. Add .unread to mark it as new. Update the badge count.
  5. 5
    Fire an API on mark-all-readIn the JS panel, add a fetch() call inside markAll() after the DOM changes to sync the read state with your backend.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Dashboard and app header notifications
Add to any dashboard header. Wire the badge count to your real-time notification API response and update it via WebSocket or polling.
Social and collaboration tools
Notify users of new mentions, comments, team activity, and messages. The .unread class and mark-all-read pattern match what users expect from social products.
Learn click-outside close pattern
The click-outside handler uses e.target.closest(".bell-wrap"). Edit the selector to understand how closest() traverses the DOM.
Prototype notification UX
Use the snippet to prototype how notification volume, grouping, and read/unread states should work before building a production notification system.
Customise the badge and panel styling
Change the badge colour, panel width, and notification item layout in the CSS panel to match your design system.
Real-time notification updates
On a WebSocket message, increment the badge count and prepend a new .notif.unread item to the list. The CSS handles the visual state automatically.

Got questions?

Frequently Asked Questions

toggle() calls panel.classList.toggle("open"). The CSS .panel.open rule transitions opacity from 0 to 1 and transform from translateY(-8px) to translateY(0) over 0.2s. pointer-events: none when closed prevents invisible click interception.

markAll() queries all .notif.unread elements. For each: it removes .unread, finds and removes the .notif-dot element inside it. After the loop, it checks if any .unread remain — if none, it hides the badge element.

A document click listener checks !e.target.closest(".bell-wrap"). If the click was outside the bell wrapper (no ancestor with that class), panel.classList.remove("open") closes the panel. This is the same pattern as the Dropdown Menu snippet.

Fetch notifications from your API and update the badge textContent with the unread count. Add .unread class to unread notification elements. Show or hide the badge based on count > 0.

Listen to a WebSocket or Server-Sent Events stream. On new notification, create a .notif div with .unread, prepend it to the notification list, increment the badge count, and show the badge if hidden.

Yes. Click "JSX" for a React component. Manage open state with useState, notifications as an array with useState, and the unread count derived from filter. Use useEffect to add and clean up the document click listener.