Inbox screen

ProReact Native · Expo

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.

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/inbox-screen

Then run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/inbox-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)/messages.tsx
import { useState } from "react";
import { Stack, router } from "expo-router";
import { InboxScreen, type InboxConversation } from "@/components/goodui-native/inbox-screen/inbox-screen";
import { api } from "@/lib/api";

export default function MessagesRoute() {
  const [conversations, setConversations] = useState<InboxConversation[]>(api.cachedConversations());
  return (
    <>
      <Stack.Screen options={{ headerShown: false }} />
      <InboxScreen
        conversations={conversations}
        onOpenConversation={(c) => router.push(`/chat/${c.id}`)}
        onRefresh={async () => {
          const fresh = await api.conversations();
          setConversations(fresh);
          return fresh;
        }}
        onArchive={(id) => api.archive(id)}
        onDelete={(id) => api.remove(id)}
        onRestore={(id) => api.restore(id)}
        onMuteChange={(id, muted) => api.mute(id, muted)}
        onCompose={() => router.push("/chat/new")}
      />
    </>
  );
}

When to use

Use it for
  • The conversation list of a messaging, support or marketplace app.
  • Any list of threads with unread counts that people triage with swipes: DMs, support tickets, team chats.
Reach for something else
  • Notifications or activity: use notifications-screen, which groups by day and carries inline actions.
  • Email with subjects and long previews: compose swipe-actions and list rows yourself; this row is built for one-line names and short previews.
  • Thousands of conversations: the rows are plain views, so page the data (or move the row into a FlatList) past a few hundred.

Examples

On its own, with sample conversations

<InboxScreen />

Times in German

<InboxScreen conversations={conversations} locale="de-DE" title="Nachrichten" />

API reference

PropTypeDefault
conversations
Conversations to show. The screen keeps its own copy for swipes and undo and takes a new array whenever this one changes, so keep it stable (state or useMemo), not a new literal on every render.
InboxConversation[]sampleConversations
title
Large title. Default "Messages". Same as `labels.title`.
string—
onOpenConversation
A row was tapped. The row is marked read on screen at the same time.
(conversation: InboxConversation) => void—
onRefresh
Pull to refresh. Resolve with the fresh list and every conversation that was not on screen before drops in at the top; resolve with nothing to keep the list. A rejection shows the pull indicator's error.
() => Promise<InboxConversation[] | void> | InboxConversation[] | void—
onArchive
A conversation was archived (swipe left, or a long swipe). A toast then offers Undo for 4 seconds.
(id: string) => void—
onDelete
A conversation was deleted from the swipe actions. A toast then offers Undo for 4 seconds.
(id: string) => void—
onRestore
Undo was pressed after an archive or delete: the conversation is back on screen.
(id: string) => void—
onMuteChange
Mute was toggled from the swipe actions (swipe right).
(id: string, muted: boolean) => void—
onCompose
Shows a compose button in the bar.
() => void—
locale
BCP 47 locale for times and dates. Defaults to the device locale.
string—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<InboxScreenLabels>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • The large title fades into the bar as the list scrolls, with the hairline arriving last.
  • Pull to refresh: the drop stretches, snaps round past the threshold, spins and ends on a check.
  • New conversations after a refresh (or one brought back with Undo): the row's height opens on the bouncy spring so the rows below glide down, while the row drops in from above and fades in.
  • Archive and delete slide the row out and collapse its height before it leaves.
  • Reduced motion: new rows take their place at once and fade in; the title cross-fades without moving.
  • Haptic: tap past the pull threshold, success or error when the refresh settles (pull-to-refresh)
  • Haptic: select when a swipe crosses the long-swipe threshold, tap when a swipe action runs, delete for Delete (swipe-actions); notify for the toast, undo on its Undo
  • Haptic: tap on the compose button
  • Scroll, pull, swipes and the drop-in run on the UI thread; React renders when the list or the query changes.
  • Rows are plain views inside the pull to refresh scroll view: fine for a few hundred conversations; page beyond that.

Accessibility

  • Each conversation is one button whose label starts with the name, then the preview, the time, the unread count and 'muted', so the web name starts with the visible text.
  • Mute, archive and delete are in each row's screen reader actions menu, so nobody has to swipe.
  • The search field, the compose button ('New message') and the pull to refresh strip (a Refresh button on the web and with a screen reader) are reachable by keyboard and screen reader.
  • New conversations from a refresh are announced ('New message from Maya Patel').
  • Unread badges are decorative because the row label already says the count.

Notes for AI

  • Pass conversations with Date objects in time; the screen sorts them newest first and formats times with Intl (time today, 'Yesterday', weekday this week, day and month before).
  • Keep the conversations array stable (state or useMemo). A new array replaces what is on screen, including rows the person just archived.
  • onRefresh should resolve with the new list (the screen diffs ids to animate new rows) or with nothing to keep the list.
  • Archive and delete remove the row on screen and call onArchive or onDelete right away; Undo puts it back and calls onRestore. Do the server call in those callbacks.
  • Tapping a row marks it read on screen and calls onOpenConversation; navigate there.
  • Render it full-bleed with the navigator header hidden (headerShown: false); it handles the safe area itself.
  • It needs <Toaster /> once in the app for the Undo toast.