NotificationStack
Swipe-to-dismiss stack of notification cards with a skip button, auto-dismissing actions and automatic load-more.
Installation
pnpm dlx @docyrus/cli add @docyrus/rn-notification-stackpnpm add react-native-gesture-handler react-native-reanimated react-native-worklets react-native-svgThe stack uses react-native-gesture-handler, so the app must be wrapped in <GestureHandlerRootView> (Expo Router apps already are).
Usage
import { NotificationStack } from '@/components/docyrus-native/notification-stack';
import type { NotificationItem } from '@/components/docyrus-native/notification-stack';
<NotificationStack
notifications={unread}
onDismiss={id => markAsRead(id)}
onClick={id => openNotification(id)}
onAction={(id, action) => runAction(id, action)}
onLoadMore={() => fetchNextPage()}
/>The top card is interactive; the cards behind it are scaled down (5 % per level), offset 10 px lower and faded (15 % per level), and the container reserves bottom padding for those offsets. Dismissing the top card:
- Swipe it left or right past 100 px, or fling it faster than 500 px/s — it rotates (±8°) and fades while dragged, then flies out. A shorter swipe springs back. Vertical drags are ignored so a parent
ScrollViewkeeps scrolling. - Skip it with the ✕ button in its top-right corner.
- Act on it — pressing an action button fires the action's
onClick, thenonAction, then dismisses the card.
Every dismissal calls onDismiss(id). The stack tracks dismissed ids internally, so it works without removing items from notifications. When the remaining cards drop to loadMoreThreshold, onLoadMore fires once; it re-arms after more notifications arrive. New cards enter with a fade-down, dismissed ones leave with a fade-right (Reanimated layout animations).
Single card
import { NotificationStackCard } from '@/components/docyrus-native/notification-stack';
<NotificationStackCard
notification={notification}
onDismiss={id => remove(id)}
onAction={(id, action) => handle(id, action)}
/>API Reference
NotificationStackProps
| Prop | Type | Default | Description |
|---|---|---|---|
notifications | NotificationItem[] | — | Notifications to stack (see NotificationsPanel → NotificationItem). |
maxVisible | number | 10 | Maximum cards rendered in the stack. |
loadMoreThreshold | number | 2 | When the remaining (not dismissed) card count reaches this value, onLoadMore fires once. |
onLoadMore | () => void | — | Fetch more notifications. |
onDismiss | (id: string) => void | — | Fired on swipe, skip button, or after an action. |
onClick | (id: string) => void | — | Fired when the top card body is pressed. |
onAction | (id: string, action: string) => void | — | Fired with the action label; the card is auto-dismissed afterwards. |
variant | 'default' | 'bordered' | 'default' | bordered gives every card a medium shadow. |
size | 'sm' | 'default' | 'lg' | 'default' | Card padding, icon size and text size. |
emptyState | ReactNode | dashed "No notifications" box | Shown when no cards remain. Default text: t('ui.notificationStack.empty', 'No notifications'). |
className | string | — | Additional classes for the root (max width 672, centred). |
NotificationStackCardProps
| Prop | Type | Default | Description |
|---|---|---|---|
notification | NotificationItem | — | The notification to render. |
size | 'sm' | 'default' | 'lg' | 'default' | Card density. |
onDismiss | (id: string) => void | — | Swipe / skip callback. The ✕ button is only rendered when this is set (accessibility label t('ui.notificationStack.dismiss', 'Dismiss')). |
onClick | (id: string) => void | — | Card body press. |
onAction | (id: string, action: string) => void | — | Action button press. |
swipeEnabled | boolean | true | Enable swipe-to-dismiss (the stack enables it on the top card only). |
className | string | — | Additional card classes. |
Components
| Component / export | Description |
|---|---|
NotificationStack | The stack. |
NotificationStackCard | One swipeable card (web parity export). |
notificationStackVariants | tv() slots (base, card) for the stack. |
notificationStackCardVariants | tv() slots for the card (base, icon, badge, title, text, timestamp) by size. |
Type Exports
| Type | Description |
|---|---|
NotificationStackProps | Props for NotificationStack. |
NotificationStackCardProps | Props for NotificationStackCard. |
NotificationItem | Re-exported from notifications-panel (now carries badge: ReactNode). |
NotificationAction | Re-exported from notifications-panel (now carries icon: ReactNode). |
NotificationStackVariant | 'default' | 'bordered'. |
NotificationStackSize | 'sm' | 'default' | 'lg'. |
StackNotificationItem | Deprecated alias of NotificationItem. |
StackNotificationAction | Deprecated alias of NotificationAction. |
Breaking changes
- Now a multi-file component (
components/notification-stack/). The import path@/components/docyrus-native/notification-stackis unchanged; reinstall withdocyrus add rn-notification-stackso the old single file is replaced by the folder. maxVisibledefault3→10(web parity), and it now caps the rendered stack instead of a list with "N more".- The "N more" link under the stack was removed —
onLoadMorefires automatically once the remaining cards reachloadMoreThreshold(and only once until more arrive, previously on every change). - Actions now auto-dismiss the card and call
onDismiss(web parity). - Stacked-card visual replaces the front card + edge shells; the dismiss button moved to the card's top-right corner and cards can be swiped away.
StackNotificationItem/StackNotificationActionare deprecated aliases —badgeis nowReactNode(wasstring | number) and lives onNotificationItemitself, and the badge sits at the bottom-right of the icon in the type colour (was a red top-right counter).- Requires
react-native-worklets(already a peer of Reanimated 4) and aGestureHandlerRootViewancestor.