Avatar
Free · MITReact Native · ExpoA 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.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/avatar.jsonThen 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
- 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.
- 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
| Prop | Type | Default |
|---|---|---|
| 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. | boolean | false |
| 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
| Prop | Type | Default |
|---|---|---|
| people* Everyone in the group, in display order. | AvatarGroupPerson[] | — |
| max Avatars shown before the rest collapse into a "+N" counter. Default 4. | number | 4 |
| 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. | boolean | true |
| 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.
Related
An Instagram-style story viewer that grows out of the tapped avatar: segmented bars fill at the top, tap right or left to step, hold to pause while the chrome steps aside, swipe sideways to the next person with a plain slide, and drag down to send it back into the avatar.
A count, dot or status badge that springs in when it appears, pops when its value changes, caps at 99+ and shrinks away at zero; BadgeAnchor pins it to an icon or avatar.
Message bubbles that group into runs with one tail, carry a receipt that ticks forward (clock, a check that draws itself, two checks, both brightening when read) and spring up out of the composer; plus a TypingIndicator whose dots wave and a Composer whose mic morphs into a filled send arrow as soon as there is text.
Long-press a message and a pill of emoji springs out of it one by one; slide the finger across and the emoji under it swells dock-style while its neighbours lean in; let go to pick and the emoji flies in an arc onto the message, landing in a badge with a pop.