Detent web components
By Shane Logsdon

Feedback & status

Notification Bell

Implemented

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

dot mode
99+ truncation
empty state

Recorded states

  • open recorded state; no setter
  • closed recorded state; no setter
  • expanded recorded 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-open
  • show-count

Properties

  • notifications
  • isOpen
  • showCount
  • onMarkAsRead
  • onMarkAllAsRead
  • onOpenChange

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.tsx sha256 bcc937e008f3…

Native files

The native module and its artifacts:

  • src/components/notification-bell/notification-bell.ts
  • src/components/notification-bell/styles.css
  • src/components/notification-bell/define.ts
  • docs/components/notification-bell.md
  • site/examples/notification-bell.ts
  • tests/browser/notification-bell.spec.ts
  • evidence/components/notification-bell.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
is-openisOpenbooleanundefinedPresence semantics: present means true, absent means false.
show-countshowCountbooleantruePresence semantics: present means true, absent means false.

Properties

Properties
PropertyTypeDefaultNotes
notificationsNotification[]—Property-only: assign from script; not accepted as an attribute.
isOpenbooleanundefinedProperty-only: assign from script; not accepted as an attribute.
showCountbooleantrueProperty-only: assign from script; not accepted as an attribute.
onMarkAsRead(id: string) => void—Property-only: assign from script; not accepted as an attribute.
onMarkAllAsRead() => voidundefinedProperty-only: assign from script; not accepted as an attribute.
onOpenChange(isOpen: boolean) => voidundefinedProperty-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-mark-as-readonMarkAsRead(id: string) => voidComposed, bubbles; fires only for a user action.
detent-mark-all-as-readonMarkAllAsRead() => voidComposed, bubbles; fires only for a user action.
detent-open-changeonOpenChange(isOpen: boolean) => voidComposed, 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.
Previous: Not Found Next: Toast Stack

Docs

Components

Project