Notifications screen

ProReact Native · Expo

Activity grouped into Today and Earlier with avatars, unread dots and thumbnails, follow requests with Accept and Decline in the row, swipe to dismiss, Mark all read where the dots fold away one after another, a banner that offers to turn on push while it is off, and an empty state when everything is cleared.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add @goodui-native-pro/notifications-screen

Then run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/notifications-screen.

Pro screens install from the license-gated @goodui-native-pro registry. Setup: Pro access.

  • Wrap the app in GestureHandlerRootView and ThemeProvider, and render <Toaster /> once inside ThemeProvider.

Usage

// app/(tabs)/activity.tsx
import { Stack, router } from "expo-router";
import * as Notifications from "expo-notifications";
import { NotificationsScreen } from "@/components/goodui-native/notifications-screen/notifications-screen";
import { api, useActivity, usePushPermission } from "@/lib/activity";

export default function ActivityRoute() {
  const notifications = useActivity(); // AppNotification[], newest first, time as Date
  const pushEnabled = usePushPermission();
  return (
    <>
      <Stack.Screen options={{ headerShown: false }} />
      <NotificationsScreen
        notifications={notifications}
        pushEnabled={pushEnabled}
        onEnablePush={async () => {
          const { granted } = await Notifications.requestPermissionsAsync();
          if (!granted) throw new Error("denied");
        }}
        onAccept={(id) => api.acceptFollow(id)}
        onDecline={(id) => api.declineFollow(id)}
        onDismiss={(id) => api.dismiss(id)}
        onMarkAllRead={() => api.markAllRead()}
        onOpen={(n) => router.push(`/activity/${n.id}`)}
      />
    </>
  );
}

When to use

Use it for
  • The activity or notifications tab of a social, community or collaboration app.
  • Any list of events where some need an answer in place (requests, invites, approvals) and the rest can be dismissed.
Reach for something else
  • Conversations: use inbox-screen, which sorts threads by their latest message.
  • A one-off message after an action: use toast; for a persistent status on one screen use banner.
  • Thousands of notifications: rows are plain views; page older activity.

Examples

On its own, with sample activity and the push banner

<NotificationsScreen />

Thumbnails for posts

<NotificationsScreen
  notifications={[
    { id: "1", kind: "like", actor: { name: "Maya Patel" }, text: "liked your photo.", time: new Date(), thumbnail: post.imageUrl },
  ]}
  pushEnabled
/>

API reference

PropTypeDefault
notifications
Notifications, newest first. The screen keeps its own copy for read state, dismissals and requests and takes a new array whenever this one changes, so keep it stable (state or useMemo).
AppNotification[]sampleNotifications
pushEnabled
Whether push notifications are allowed. While false, a banner offers to turn them on. Default false.
booleanfalse
onEnablePush
"Turn on" in the banner. Return a promise (for example the permission request); the banner closes when it resolves.
() => unknown—
onAccept
Accept on a follow request. Return a promise: the button spins, then the row says the person now follows you.
(id: string) => unknown—
onDecline
Decline on a follow request. Return a promise: the button spins, then the row leaves.
(id: string) => unknown—
onDismiss
A row was swiped away.
(id: string) => void—
onOpen
A row was tapped (not follow requests, which have their own buttons). It is marked read at the same time.
(notification: AppNotification) => void—
onMarkAllRead
"Mark all read" was pressed.
() => void—
title
Large title. Default "Notifications". Same as `labels.title`.
string—
onBack
Shows a back button in the bar.
() => void—
locale
BCP 47 locale for dates older than a week. Defaults to the device locale.
string—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<NotificationsScreenLabels>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • Mark all read: the unread dots fold away one after another, top to bottom, 45 ms apart on the snappy spring.
  • Accept gathers into a spinner and a check, then the row turns into 'is now following you'; Decline and dismiss slide the row out and collapse its height.
  • The push banner collapses and pushes the list up when it closes; the empty state's line art draws itself in when the last row goes.
  • Reduced motion: the dots fade out without the stagger, the banner changes height at once and the empty state fades in without drawing.
  • Haptic: tap on Accept, Decline, Mark all read and Turn on; success or error when their promise settles (Button, Banner)
  • Haptic: select when a swipe crosses the long-swipe threshold, tap when a swipe action runs (SwipeActions)
  • Dots, swipes and collapses run on the UI thread; React renders when the list changes.

Accessibility

  • Today and Earlier are headers. Each row reads the name, what happened, the time and 'unread'; rows that open something are buttons.
  • Follow requests offer Accept and Decline as buttons in the row and in the row's screen reader actions; other rows offer Dismiss there, so nobody has to swipe.
  • Accept and Decline report busy while their promise runs; 'All notifications marked as read' is announced.
  • The push banner is announced when it appears after mount; Turn on shows a spinner while the permission request runs.

Notes for AI

  • Pass notifications newest first with time as Date; rows from today group under Today and the rest under Earlier. text follows the name, so start it lower case ('liked your photo.').
  • Keep the array stable (state or useMemo); a new array replaces read state and dismissals on screen.
  • onAccept and onDecline may return promises; the buttons show pending and error themselves. After a resolved Accept the row says the person now follows you; after Decline it leaves.
  • Swiped declines and dismissals leave at once; a rejected swiped Accept or Decline shows an error toast (and a declined request comes back).
  • pushEnabled false shows the banner; onEnablePush should request permission and reject when it is denied, so the banner stays and shakes.
  • Render it full-bleed with the navigator header hidden (headerShown: false). It needs <Toaster /> once in the app for swipe errors.