Feedback & status
Notification Bell
Notification badge and alert drawer
detent-notification-bell
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 3
- Wave
- phase-3-overlays
- Group
- Feedback & status
7.4 KB gzip for the class module · inlines the shared spring
Variants
Recorded states
openrecorded state; no setterclosedrecorded state; no setterexpandedrecorded state; no setter
Usage
Import
import '/components/notification-bell/define.js';
// or register it yourself
import { DetentNotificationBell } from '/components/notification-bell/notification-bell.js';
Attributes
is-openshow-count
Properties
notificationsisOpenshowCountonMarkAsReadonMarkAllAsReadonOpenChange
Slots
No slots are recorded for this component.
Events
detent-mark-as-read— {}detent-mark-all-as-read— {}detent-open-change— {}
Example
Runnable example: site/examples/notification-bell.ts. The snippet below is generated from the catalogued attributes.
<detent-notification-bell
is-open
show-count
></detent-notification-bell>
Property-only values
const element = document.querySelector('detent-notification-bell');
if (element !== null) {
// notifications: Notification[]
element.notifications = [];
// onMarkAsRead: (id: string) => void
element.onMarkAsRead = () => {};
// onMarkAllAsRead: () => void
element.onMarkAllAsRead = () => {};
// onOpenChange: (isOpen: boolean) => void
element.onOpenChange = () => {};
}
Source
The pinned upstream source for this row:
src/components/ui/NotificationBell.tsxsha256 bcc937e008f3…
Native files
The native module and its artifacts:
src/components/notification-bell/notification-bell.tssrc/components/notification-bell/styles.csssrc/components/notification-bell/define.tsdocs/components/notification-bell.mdsite/examples/notification-bell.tstests/browser/notification-bell.spec.tsevidence/components/notification-bell.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
is-open | isOpen | boolean | undefined | Presence semantics: present means true, absent means false. |
show-count | showCount | boolean | true | Presence semantics: present means true, absent means false. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
notifications | Notification[] | — | Property-only: assign from script; not accepted as an attribute. |
isOpen | boolean | undefined | Property-only: assign from script; not accepted as an attribute. |
showCount | boolean | true | Property-only: assign from script; not accepted as an attribute. |
onMarkAsRead | (id: string) => void | — | Property-only: assign from script; not accepted as an attribute. |
onMarkAllAsRead | () => void | undefined | Property-only: assign from script; not accepted as an attribute. |
onOpenChange | (isOpen: boolean) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-mark-as-read | onMarkAsRead | (id: string) => void | Composed, bubbles; fires only for a user action. |
detent-mark-all-as-read | onMarkAllAsRead | () => void | Composed, bubbles; fires only for a user action. |
detent-open-change | onOpenChange | (isOpen: boolean) => void | Composed, bubbles; fires only for a user action. |
Slots
No slots are recorded for this component.
CSS customisation
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- React children/render props do not exist here; the panel is fully rendered from notifications. The row records no slots, so none were added.
- onMarkAsRead, onMarkAllAsRead and onOpenChange become composed, bubbling custom events (detent-mark-as-read, detent-mark-all-as-read, detent-open-change); programmatic writes emit none.
- isOpen is no longer a controlled-with-callback pair: the element owns the state, reflects is-open, and reports user changes. Reflecting the event value back is idempotent, which is the native equivalent of the controlled pattern.
- The floating panel is a native popover in the top layer instead of an absolutely positioned motion.div. Position is computed from the trigger box on open and on viewport resize/scroll because CSS anchor positioning is not available on every supported engine; the panel flips above the trigger when it does not fit below.
- Framer Motion AnimatePresence scale/opacity springs become the shared spring helper with the pinned springSnappy token (380/30/0.5) for the badge and the upstream 0.5 s keyframe sequence for the arrival shake. Reduced motion removes both and keeps the final state.
- lucide-react icons (Bell, CheckCheck, X) become local decorative inline SVGs with the same meaning (upstream ISC notice retained in THIRD_PARTY_NOTICES.md).
- The “Read all” control renders whenever something is unread; upstream required an onMarkAllAsRead callback to be passed, which has no native equivalent.
- The item action?: () => void field is dropped: the pinned upstream render never used it, and a function cannot be plain event/detail data (PC-6.3).
- Accessibility corrections: the panel is a named non-modal dialog (upstream was an unlabelled div), the badge text is part of the accessible name (WCAG 2.5.3), the read message keeps full contrast instead of upstream's dimmed text-[#A1A1A1], and the trigger exposes a :focus-visible ring from the --detent-focus-ring token instead of a Tailwind ring class.
- The mobile panel width is min(20rem, 100vw - 1rem) and it is anchored to the trigger's right edge with a viewport clamp, instead of upstream's left-1/2 -translate-x-1/2 mobile centring.
What it does
A bell trigger with an unread badge and a non-modal notification panel. The panel is a native popover (top layer), so it is never clipped by an ancestor overflow, transform or z-index, and it is dismissed by Escape or an outside activation.
Upstream behaviour
- Subtle rotational bell shake when new unread notifications arrive
- Spring-animated counter badge with overflow truncation (99+)
- Floating backdrop-blur panel with individual mark-as-read and mark-all-read controls
- Click-outside dismissal and accessible keyboard escape handling
Differences from upstream
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- React children/render props do not exist here; the panel is fully rendered from notifications. The row records no slots, so none were added.
- onMarkAsRead, onMarkAllAsRead and onOpenChange become composed, bubbling custom events (detent-mark-as-read, detent-mark-all-as-read, detent-open-change); programmatic writes emit none.
- isOpen is no longer a controlled-with-callback pair: the element owns the state, reflects is-open, and reports user changes. Reflecting the event value back is idempotent, which is the native equivalent of the controlled pattern.
- The floating panel is a native popover in the top layer instead of an absolutely positioned motion.div. Position is computed from the trigger box on open and on viewport resize/scroll because CSS anchor positioning is not available on every supported engine; the panel flips above the trigger when it does not fit below.
- Framer Motion AnimatePresence scale/opacity springs become the shared spring helper with the pinned springSnappy token (380/30/0.5) for the badge and the upstream 0.5 s keyframe sequence for the arrival shake. Reduced motion removes both and keeps the final state.
- lucide-react icons (Bell, CheckCheck, X) become local decorative inline SVGs with the same meaning (upstream ISC notice retained in THIRD_PARTY_NOTICES.md).
- The “Read all” control renders whenever something is unread; upstream required an onMarkAllAsRead callback to be passed, which has no native equivalent.
- The item action?: () => void field is dropped: the pinned upstream render never used it, and a function cannot be plain event/detail data (PC-6.3).
- Accessibility corrections: the panel is a named non-modal dialog (upstream was an unlabelled div), the badge text is part of the accessible name (WCAG 2.5.3), the read message keeps full contrast instead of upstream's dimmed text-[#A1A1A1], and the trigger exposes a :focus-visible ring from the --detent-focus-ring token instead of a Tailwind ring class.
- The mobile panel width is min(20rem, 100vw - 1rem) and it is anchored to the trigger's right edge with a viewport clamp, instead of upstream's left-1/2 -translate-x-1/2 mobile centring.