Notifications screen
ProReact Native · ExpoActivity 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.
Installation
$ npx shadcn@latest add @goodui-native-pro/notifications-screenThen 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
- 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.
- 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
| Prop | Type | Default |
|---|---|---|
| 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. | boolean | false |
| 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.
Related
A complete messaging inbox: a large title that collapses into the bar, a search field, conversation rows with avatars, previews, times and unread badges, swipe to mute, archive or delete with Undo, and pull to refresh where new conversations drop in at the top and push the list down.
Row actions that unfold with the finger; a long swipe lets the edge action take over and run, and destructive actions collapse the row before it leaves.
An inline status banner that slides down from under the edge above and pushes the content down, flings away sideways with the finger, and can collapse on its own behind a draining hairline; includes an offline preset.
A designed empty, error or offline state: a tinted disc pops in, its line art draws itself stroke by stroke with the highlight landing last, then the title, description and actions rise in.
A photo that fades in over initials on a color picked from the name, with a presence dot and a story ring whose accent arc sweeps in when there is something new and away once seen; AvatarGroup stacks people with a +N counter and fans out on tap.
A press-responsive button with haptics that runs async actions on its own: it gathers into a spinner, then opens into a success or error state.