Avatar

Free · MITReact Native · Expo

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.

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 https://goodui.dev/native/r/avatar.json

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

Usage

import { Avatar, AvatarGroup } from "@/components/goodui-native/avatar/avatar";

export function Assignees() {
  return (
    <>
      <Avatar name="Maya Patel" source="https://example.com/maya.jpg" size="lg" status="online" />
      <AvatarGroup
        max={3}
        people={[
          { name: "Maya Patel", source: "https://example.com/maya.jpg" },
          { name: "Leo Martins" },
          { name: "Aiko Tanaka" },
          { name: "Sam Okafor" },
        ]}
      />
    </>
  );
}

When to use

Use it for
  • A person, team or company next to their name: contact rows, comments, chat headers, assignees.
  • A story tray: pass story='unseen' or 'seen' and an onPress that opens Stories.
  • Presence in messaging or support tools: online, away, busy, offline.
  • Showing who is in a meeting, project or event with AvatarGroup.
Reach for something else
  • A product or content photo: use an Image, or ImageThumbnail from image-viewer when it should open full screen.
  • A count or status on its own: use badge.
  • Picking people from a list: use chip (input chips with a leading avatar) or a list-row.

Examples

Story tray that opens Stories from the tapped avatar

const refs = useRef<(ComponentRef<typeof View> | null)[]>([]);
const origin = useRef<ComponentRef<typeof View> | null>(null);

<Avatar
  ref={(el) => { refs.current[i] = el; }}
  name={user.name}
  source={user.avatar}
  size="lg"
  story={allSeen ? "seen" : "unseen"}
  onPress={() => { origin.current = refs.current[i]; setOpen(true); }}
/>
<Stories users={users} open={open} onClose={() => setOpen(false)} initialUser={i} originRef={origin} />

Company logo with a square shape

<Avatar name="Northwind" shape="square" source={logo} />

A group that opens the member list instead of fanning out

<AvatarGroup people={members} max={4} onPress={() => router.push("/members")} />

API reference

Avatar

PropTypeDefault
name
The person's name. Gives the initials, the fallback color (always the same for the same name) and the screen reader label.
string—
source
Photo. It fades in over the initials once it has loaded; if it fails the initials stay.
AvatarSource—
size
24, 32, 40, 56 or 80 pt.
"sm" | "md" | "xs" | "lg" | "xl""md"
shape
Circle, or a rounded square for companies, teams and apps.
"circle" | "square""circle"
status
Presence dot on the bottom edge: online (green), away (amber), busy (red) or offline (grey).
"online" | "away" | "busy" | "offline"—
pulse
The online dot breathes outward. Off under reduced motion.
booleanfalse
story
Story ring around the avatar. `unseen` draws a full accent ring, `seen` a thin grey one. Switching between them sweeps the accent arc in or out around the circle.
"unseen" | "seen"—
ring
Surface the presence dot cuts out of. Match what the avatar sits on.
"background" | "surface" | "surfaceMuted" | "surfaceRaised""surface"
onPress
Makes the avatar a button (open a profile, play stories). It sinks on touch down inside its ring.
() => void—
haptic
Haptic on press. `false` turns it off.
HapticName | false"tap"
accessibilityLabel
Read by screen readers. Defaults to the name plus status and story state.
string—
ref
Ref to the outer view, for example to open Stories from this avatar (`originRef`).
Ref<ComponentRef<typeof View>>—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<AvatarLabels>—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

AvatarGroup

PropTypeDefault
people*
Everyone in the group, in display order.
AvatarGroupPerson[]—
max
Avatars shown before the rest collapse into a "+N" counter. Default 4.
number4
size"sm" | "md" | "xs" | "lg" | "xl""sm"
ring
Surface the overlapping avatars are cut out of. Match what the group sits on.
"background" | "surface" | "surfaceMuted" | "surfaceRaised""surface"
expandable
Tap fans the stack out into a row and back. Default `true`. Ignored when `onPress` is set.
booleantrue
expanded
Fanned out (controlled).
boolean—
onExpandedChange
Called when a tap fans the stack out or folds it.
(expanded: boolean) => void—
onPress
Replaces the fan-out with your own action, for example opening the member list.
() => void—
haptic
Haptic on press. `false` turns it off.
HapticName | false"tap"
accessibilityLabel
Read by screen readers. Defaults to "Ana, Ben, Cem and 3 others".
string—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<AvatarGroupLabels>—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • The photo fades in over the initials on the slow timing once it has loaded; a photo that fails to load leaves the initials.
  • Story ring: switching to unseen sweeps the accent arc around from twelve o'clock over 650 ms; switching to seen retracts it after a short beat, leaving the thin grey ring. Nothing animates on mount.
  • A pressable avatar sinks to 0.92 inside its ring on the snappy spring from touch down.
  • AvatarGroup fans out on the smooth spring; each avatar starts a beat after the one before it and lifts slightly at the middle of its travel, and the group's width follows on the UI thread.
  • The online dot can pulse (Badge pulse).
  • Reduced motion: the photo appears without fading, the ring crossfades instead of sweeping, there is no press sink, the group jumps between folded and fanned, and the dot does not pulse.
  • Haptic: tap when a pressable Avatar or AvatarGroup is pressed (configurable with haptic, false turns it off)
  • The fade, the ring sweep, the press and the fan run on the UI thread with Reanimated. The ring is one SVG circle whose dash offset animates; no re-renders while it moves.
  • Color from the name is a small string hash, computed during render with no state.

Accessibility

  • Without onPress an Avatar is an image labelled with the name, the status and 'new story' (for example 'Maya Patel, online, new story'); the initials and dot are not read separately.
  • With onPress it is a button with the same label and a 44 pt minimum touch target through hitSlop.
  • AvatarGroup is read as one element: 'Maya Patel, Leo Martins, Aiko Tanaka and 5 others'. When it fans out it is a button with the expanded state (aria-expanded on the web).
  • Initials keep their size at large text settings so they never clip; the name is in the label.
  • Initials sit on an accent family's soft fill in that family's accent color. Only families that keep 4.5:1 in light and dark are used (violet, blue, green, amber, orange).

Notes for AI

  • Always pass name when you know it: it gives the initials, the stable color and the screen reader label.
  • source takes a URL string or any React Native image source; do not wrap a URL in { uri } yourself (both work).
  • story is 'unseen' | 'seen' | undefined (no ring). Flip it to 'seen' after the person watched the stories; the sweep is the feedback.
  • status and pulse are for presence only. Use ring to match the surface the avatar sits on so the dot cuts out cleanly.
  • Sizes are named (xs 24, sm 32, md 40, lg 56, xl 80); there is no numeric size prop.
  • AvatarGroup fans out by default; pass onPress to replace that, or expandable={false} for a static stack.
  • Use avatarAccent(name) and initialsOf(name) if you need the same color or initials elsewhere.