Detent web components
By Shane Logsdon

Feedback & status

Toast Stack

Implemented

Swipe-to-dismiss toast stack

detent-toast-stack

Port status
Catalog status
implemented
Origin
upstream
Phase
3
Wave
phase-3-overlays
Group
Feedback & status

8.8 KB gzip for the class module · inlines the shared spring

Variants

maxVisible = 1
empty (closed state)

Recorded states

  • closed recorded state; no setter
  • dragging recorded state; no setter

Usage

Import

import '/components/toast-stack/define.js';

// or register it yourself
import { DetentToastStack } from '/components/toast-stack/toast-stack.js';

Attributes

  • max-visible
  • closed
  • expanded
  • dragging

Properties

  • initialNotifications

Slots

No slots are recorded for this component.

Events

No events are recorded for this component.

Example

Runnable example: site/examples/toast-stack.ts. The snippet below is generated from the catalogued attributes.

<detent-toast-stack
  max-visible="3"
  closed
  expanded
  dragging
></detent-toast-stack>

Property-only values

const element = document.querySelector('detent-toast-stack');
if (element !== null) {
  // initialNotifications: NotificationItem[]
  element.initialNotifications = [];
}

Source

The pinned upstream source for this row:

  • src/components/ui/NotificationStack.tsx sha256 89664571b64b…

Native files

The native module and its artifacts:

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

API

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

Attributes

Attributes
AttributePropertyTypeDefaultNotes
max-visiblemaxVisiblenumber3Kebab-case attribute; camelCase property.
closed———Recorded in the catalog as an attribute.
expanded———Recorded in the catalog as an attribute.
dragging———Recorded in the catalog as an attribute.

Properties

Properties
PropertyTypeDefaultNotes
initialNotificationsNotificationItem[][]Property-only: assign from script; not accepted as an attribute.

Events

No custom events are recorded for this component.

Slots

No slots are recorded for this component.

CSS customisation

  • 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.
  • className is replaced by the documented parts and custom properties.
  • React state and AnimatePresence do not exist: the element owns the list, and the row records no events, so no callback or event was invented. The only announcement is the polite status region the row's accessibility note asks for (upstream declared one in its meta but never implemented it).
  • Framer Motion drag physics become a direct pointer-follow offset plus the shared spring helper with the pinned upstream springResponsive token (320/24/0.7) for the rebound. Reduced motion applies the rest value with no frames.
  • The exit (opacity 0, x: 80, scale: 0.9) runs on springResponsive (320/24/0.7) through the shared spring helper, with opacity clamped to [0, 1], as upstream's AnimatePresence popLayout: the item leaves the list at once, its card leaves the layout and becomes inert (focus moves to the next card's dismiss button or the toggle) and is removed when the spring settles. It is skipped under reduced motion; a disconnect removes it.
  • Cards are keyed by id and every card's offset, scale and opacity springs on springResponsive to its slot on add, dismiss and slice changes, entering from upstream's initial { opacity: 0, y: -20, scale: 0.95 } (first paint included, as upstream sets no initial={false}). The empty placeholder fades in on framer-motion's default 0.3 s opacity tween. Expand and Stack glide each card between its stack slot and its column place on springResponsive (Framer layout, vertical position only, through src/motion/layout-flip.ts), as do column-mode adds and dismisses.
  • Expanded mode lays the cards out in a normal column with a gap instead of the upstream fixed y: index * 74 offset, which overlapped cards taller than 74 px.
  • “+ Simulate” adds one of three canned notifications in rotation instead of a Math.random() choice, so the behaviour is reproducible and testable.
  • lucide-react icons (CheckCircle2, AlertCircle, Info, X, Bell) become local decorative inline SVGs (upstream ISC notice retained in THIRD_PARTY_NOTICES.md).
  • Accessibility corrections: the dismiss button carries an explicit focus ring, the counter/type information is exposed as text (upstream used heading levels inside a card list, which cannot be given a level from inside a component safely), and the new status region announces add/dismiss once.

What it does

A stack of notification cards: the newest card sits on top, the others are offset and scaled behind it, a card can be swiped to the right to dismiss it, and an “Expand” control reveals the whole history.

Upstream behaviour
  • Interactive drag-to-dismiss with spring rebound
  • Dynamic stacking elevation offset and scale
  • Expandable history view
Differences from upstream
  • 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.
  • className is replaced by the documented parts and custom properties.
  • React state and AnimatePresence do not exist: the element owns the list, and the row records no events, so no callback or event was invented. The only announcement is the polite status region the row's accessibility note asks for (upstream declared one in its meta but never implemented it).
  • Framer Motion drag physics become a direct pointer-follow offset plus the shared spring helper with the pinned upstream springResponsive token (320/24/0.7) for the rebound. Reduced motion applies the rest value with no frames.
  • The exit (opacity 0, x: 80, scale: 0.9) runs on springResponsive (320/24/0.7) through the shared spring helper, with opacity clamped to [0, 1], as upstream's AnimatePresence popLayout: the item leaves the list at once, its card leaves the layout and becomes inert (focus moves to the next card's dismiss button or the toggle) and is removed when the spring settles. It is skipped under reduced motion; a disconnect removes it.
  • Cards are keyed by id and every card's offset, scale and opacity springs on springResponsive to its slot on add, dismiss and slice changes, entering from upstream's initial { opacity: 0, y: -20, scale: 0.95 } (first paint included, as upstream sets no initial={false}). The empty placeholder fades in on framer-motion's default 0.3 s opacity tween. Expand and Stack glide each card between its stack slot and its column place on springResponsive (Framer layout, vertical position only, through src/motion/layout-flip.ts), as do column-mode adds and dismisses.
  • Expanded mode lays the cards out in a normal column with a gap instead of the upstream fixed y: index * 74 offset, which overlapped cards taller than 74 px.
  • “+ Simulate” adds one of three canned notifications in rotation instead of a Math.random() choice, so the behaviour is reproducible and testable.
  • lucide-react icons (CheckCircle2, AlertCircle, Info, X, Bell) become local decorative inline SVGs (upstream ISC notice retained in THIRD_PARTY_NOTICES.md).
  • Accessibility corrections: the dismiss button carries an explicit focus ring, the counter/type information is exposed as text (upstream used heading levels inside a card list, which cannot be given a level from inside a component safely), and the new status region announces add/dismiss once.
Previous: Notification Bell Next: Orbital Loader

Docs

Components

Project