Profile screen

ProReact Native · Expo

A complete social profile: the handle in the bar, an avatar with a story ring that sweeps to seen, post, follower and following counts that roll up from zero as the screen opens, bio, location and link, Follow and Message buttons, and swipeable tabs with a photo grid that opens full screen and a list.

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

Then run npx expo install --fix so react-native-svg match your Expo SDK. Files land in components/goodui-native/profile-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 for the follow error toast.

Usage

// app/profile/[username].tsx
import { Stack, router, useLocalSearchParams } from "expo-router";
import { ProfileScreen } from "@/components/goodui-native/profile-screen/profile-screen";
import { api, useProfile } from "@/lib/social";

export default function ProfileRoute() {
  const { username } = useLocalSearchParams<{ username: string }>();
  const { user, stats, photos, trips, isFollowing } = useProfile(username);
  return (
    <>
      <Stack.Screen options={{ headerShown: false }} />
      <ProfileScreen
        user={{ name: user.name, username: user.username, bio: user.bio, avatar: user.photoUrl, location: user.city, story: user.hasNewStory ? "unseen" : undefined }}
        stats={stats}
        photos={photos.map((p) => ({ source: p.url, width: p.width, height: p.height, alt: p.description }))}
        items={trips.map((t) => ({ id: t.id, title: t.place, subtitle: `${t.photoCount} photos`, value: t.month }))}
        defaultFollowing={isFollowing}
        onFollowChange={(following) => (following ? api.follow(username) : api.unfollow(username))}
        onMessage={() => router.push(`/chat/${user.id}`)}
        onStoryPress={() => router.push(`/stories/${username}`)}
        onItemPress={(item) => router.push(`/trips/${item.id}`)}
        onBack={() => router.back()}
      />
    </>
  );
}

When to use

Use it for
  • Another person's profile in a social, community, creator or marketplace app.
  • A public page for a host, coach or seller with their photos and a list of what they offer.
Reach for something else
  • Editing your own profile: use edit-profile-screen.
  • Account and app settings: use settings-screen.
  • Hundreds of photos: the grid is plain views inside a ScrollView; page the photos or move the grid into a FlatList with numColumns.

Examples

On its own, with a sample profile (the grid shows its empty state without photos)

<ProfileScreen />

Bundled photos and custom tab titles

<ProfileScreen
  photos={[
    { source: require("@/assets/lake.jpg"), alt: "A cold lake at sunrise", width: 1200, height: 900 },
    { source: require("@/assets/ridge.jpg"), alt: "A ridge trail above the clouds" },
  ]}
  tabTitles={{ photos: "Shots", items: "Routes" }}
/>

API reference

PropTypeDefault
userProfileUsersampleProfileUser
stats
Counts under the avatar. They count up from zero when the screen opens.
ProfileStatssampleProfileStats
photos
Photos in the grid tab. Tapping one opens it full screen.
ProfilePhoto[][]
items
Rows in the second tab.
ProfileListItem[]sampleProfileItems
tabTitles
Tab titles. Default "Photos" and "Trips".
{ photos: string; items: string }{ photos: "Photos", items: "Trips" }
defaultFollowing
Whether you follow this person when the screen opens.
booleanfalse
onFollowChange
Follow was toggled. The button and the follower count change at once; return a promise and a rejection rolls both back with an error toast.
(following: boolean) => unknown—
onMessage
Shows a Message button beside Follow.
() => void—
onStoryPress
Tapping the avatar while it has a story ring. The ring then sweeps to seen.
() => void—
onItemPress
Tapping a row in the second tab.
(item: ProfileListItem) => void—
onBack
Shows a back button in the bar.
() => void—
onMore
Shows a more button in the bar, for example to open an action sheet.
() => void—
locale
BCP 47 locale for the counts. Defaults to the device locale.
string—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • The counts roll up from zero digit by digit a moment after the screen opens; following rolls the follower count by one, up or down with the change.
  • Tapping an unseen story ring sweeps the accent arc away to the seen ring.
  • Photos fly out of their grid cell into the full-screen viewer and back on close; the tab indicator stretches between tabs as you swipe.
  • Reduced motion: counts change in place, the ring crossfades instead of sweeping, photos fade in and out instead of flying.
  • Haptic: tap on Follow, Message, back and more
  • Haptic: tap on the avatar when it opens stories
  • Haptic: select when the tab changes
  • Digit rolls, the ring sweep, the tab indicator and the viewer run on the UI thread.
  • Tab pages mount lazily: a page mounts once it or a neighbour is current, then stays mounted.

Accessibility

  • The handle in the bar is a header. The avatar reads the name and 'new story' while the ring is unseen; with onStoryPress it is a button.
  • Counts are real numbers for screen readers (no digit strips are read), each followed by its label.
  • Follow is a button named 'Follow Aiko Tanaka' or 'Following Aiko Tanaka'; the change is announced.
  • Tabs are a tab list with selected state; every photo is a button labelled with its alt text and opens the viewer, which supports the arrow keys and Escape on the web.
  • On the web the scrolling profile is a named region that takes keyboard focus.

Notes for AI

  • Pass photos only as image sources through props (URLs or require() from your app); the screen bundles no images. Without photos the grid tab shows an empty state.
  • stats are plain numbers; the screen formats them with Intl (pass locale to change separators) and adds or removes one follower when the person toggles Follow.
  • Return a promise from onFollowChange; a rejection rolls the button and the count back and shows an error toast, so do not add your own loading state.
  • Buttons appear only for the callbacks you pass: onBack, onMore, onMessage, onStoryPress (makes the avatar tappable), onItemPress.
  • Render it full-bleed with the navigator header hidden (headerShown: false); it handles the safe areas itself.
Avatar

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.

Rolling number

A number whose digits roll in the direction of change, with digits that open and close in place and currency signs that glide instead of jumping.

Top tabs

A swipeable pager under a tab strip whose indicator tracks the swipe continuously: the leading edge runs ahead and the trailing edge follows, so it stretches between tabs and gathers as the page lands. Labels recolor with the swipe and a scrolling strip keeps the indicator centred.

Image viewer

Tap a thumbnail and the photo flies out of it into a full-screen viewer, its crop opening up as it lands; pinch or double-tap to zoom at your fingers, drag to pan, swipe between photos, and drag down to send it back into its thumbnail while the background fades. On the web the wheel zooms toward the pointer.

Edit profile screen

Edit your own profile: change the photo from an action sheet (take, choose, remove) and watch the new one spring in, fields that validate as you leave them, Save with its own pending, saved and error states riding on the keyboard, and an alert that guards unsaved changes on back.

Chat screen

A complete one-to-one chat thread: a header with the contact's avatar, presence and call buttons, messages grouped by day and by sender, receipts that tick forward, typing dots, long-press reactions and a composer that rides the keyboard; a sent message springs up out of the composer and ticks from clock to read.