Base components
Avatar
A photo, initials or user icon for a person or a workspace, with an optional status badge, that fades the photo in once it loads and lifts each face a little on hover inside a group.
Live preview
Loading preview
A row of the six sizes with a photo, initials and an icon fallback, an online and a verified status, and an AvatarGroup of eight people with max 5 and an add button that adds a new person. Replay reloads the photos so they fade in again, and adds one person to the group.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add avatarUsage
The demo above, as code.
'use client';
import * as React from 'react';
import { Avatar, AvatarGroup, AvatarLabelGroup } from './avatar';
// Self-drawn placeholder portraits (a soft head and shoulders on a tinted card), so the demo needs no photos.
const portrait = (bg: string, fg: string) =>
`data:image/svg+xml,${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" fill="${bg}"/><circle cx="32" cy="26" r="11" fill="${fg}"/><path d="M10 64c2-13 11-21 22-21s20 8 22 21z" fill="${fg}"/></svg>`,
)}`;
const PHOTOS = [
portrait('#d9ccf5', '#8b6fd6'),
portrait('#f7d9c4', '#d08a5c'),
portrait('#c9e4dc', '#5f9e8a'),
portrait('#f3d0d8', '#c4677f'),
portrait('#cfdcf2', '#5d82c4'),
portrait('#efe3c2', '#b4974a'),
];
const PEOPLE = [
'Olivia Rhye',
'Phoenix Baker',
'Lana Steiner',
'Demi Wilkinson',
'Candice Wu',
'Natali Craig',
'Drew Cano',
'Orlando Diggs',
];
const NEW_PEOPLE = ['Andi Lane', 'Kate Morrison', 'Koray Okumus'];
const photoOf = (person: string) => PHOTOS[[...PEOPLE, ...NEW_PEOPLE].indexOf(person) % PHOTOS.length];
export default function AvatarDemo({ replay = 0 }: { replay?: number }) {
const [group, setGroup] = React.useState(PEOPLE);
const add = React.useCallback(() => {
setGroup((list) => {
const next = NEW_PEOPLE.find((n) => !list.includes(n));
return next ? [next, ...list] : list.slice(1);
});
}, []);
React.useEffect(() => {
if (replay === 0) return;
setGroup(PEOPLE);
const timer = window.setTimeout(add, 500);
return () => window.clearTimeout(timer);
}, [replay, add]);
return (
// The key remounts the avatars on Replay, so the photos load and fade in again.
<div key={replay} className="flex flex-col items-center gap-10">
<div className="flex flex-wrap items-end justify-center gap-4">
<Avatar size="xs" src={PHOTOS[0]} name="Olivia Rhye" />
<Avatar size="sm" src={PHOTOS[1]} name="Phoenix Baker" status="online" />
<Avatar size="md" src={PHOTOS[2]} name="Lana Steiner" status="verified" />
<Avatar size="lg" name="Demi Wilkinson" status="offline" />
<Avatar size="xl" placeholder="icon" name="Candice Wu" border />
<Avatar size="2xl" src={PHOTOS[3]} name="Natali Craig" count={3} />
</div>
<div className="flex flex-wrap items-center justify-center gap-4">
<Avatar shape="square" size="lg" src={PHOTOS[4]} name="Studio Lumen" />
<Avatar shape="square" size="lg" name="Studio Lumen" border />
<Avatar shape="square" size="lg" placeholder="icon" border />
</div>
<AvatarGroup size="md" max={5} onAdd={add}>
{group.map((person) => (
<Avatar key={person} name={person} src={photoOf(person)} />
))}
</AvatarGroup>
<div className="flex flex-wrap items-center justify-center gap-8">
<AvatarLabelGroup
size="md"
src={PHOTOS[0]}
name="Olivia Rhye"
supportingText="olivia@ganisigit.com"
/>
<AvatarLabelGroup
size="lg"
name="Phoenix Baker"
status="online"
supportingText="phoenix@ganisigit.com"
/>
</div>
</div>
);
}
API
Parts: root (Avatar), image, fallback (initials or user icon), border ring, status badge (online dot, offline dot, verified tick, company logo or count), group (AvatarGroup): list of avatars that overlap, group more count (+n), group add button (dashed), label group (AvatarLabelGroup): avatar, name, supporting text.
| Prop | Type | Default | Description |
|---|---|---|---|
| src | string | undefined | Photo URL. While it loads, or when it fails, the fallback shows. |
| alt | string | undefined | Accessible name of the avatar. Defaults to name. |
| name | string | undefined | Person or workspace name. Gives the initials (first letters of the first two words) and the default alt. |
| size | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "md" | 24, 32, 40, 48, 56 or 64px. AvatarGroup takes xs, sm or md. |
| shape | "circle" | "square" | "circle" | Circle for people; square with rounded corners for accounts and workspaces. |
| border | boolean | false | Adds a thin ring around the avatar. |
| placeholder | "text" | "icon" | "text" | What the fallback shows: the initials, or a user icon. With no name, the icon always shows. |
| status | "online" | "offline" | "verified" | undefined | Status badge in the bottom-right corner. Also added to the accessible name. |
| badgeSrc | string | undefined | Company logo in the corner badge (Figma status Type Avatar). Ignored when status is set. |
| count | number | undefined | Number in the corner badge (Figma status Type Count), for example unread messages. |
| max | number | 5 | AvatarGroup only: how many avatars show before the +n count. |
| groupLabel | string | "People" | AvatarGroup only: accessible name of the list. |
| supportingText | string | undefined | AvatarLabelGroup only: second line under the name, for example an email address. Hidden when not set. |
| Event | Description |
|---|---|
| loadingStatusChange | The photo started loading, loaded or failed. Payload: "loading" | "loaded" | "error". |
| add | AvatarGroup only: the add button was pressed. The add button renders only when this event has a listener. |
| Slot | Description |
|---|---|
| default | AvatarGroup: the Avatar items. Avatar: replaces the fallback content. |
| badge | Custom content for the corner badge. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| AVT1 | photo loaded | image | opacity | 0 → 1 | duration-md (260)ease-out | Shows at once |
| AVT2 | photo still loading after the delay, or failed | fallback | opacity | 0 → 1 | duration-sm (200)ease-outdelay duration-2xs (120) | Shows at once |
| AVT3 | status changes (online and offline) | status badge | background-color | old status color → new status color (fg-success-secondary or fg-quaternary) | duration-sm (200)ease-out | Swaps at once |
| AVT4 | status badge appears after first render | status badge | opacity, scale | 0, scale-swap → 1, 1 | duration-md (260)ease-out | Shows at once |
| AVT5 | hover or keyboard focus on an avatar in a group | avatar | translate y | 0 → -distance-rise | duration-sm (200)ease-out | Stays still |
| AVT6 | pointer or focus leaves the avatar | avatar | translate y | -distance-rise → 0 | duration-2xs (120)ease-out | Stays still |
| AVT7 | an avatar is added to a group | new avatar | opacity, scale | 0, scale-press → 1, 1 | duration-md (260)ease-out | Shows at once |
| AVT8 | an avatar is removed from a group | leaving avatar | opacity, scale | 1, 1 → 0, scale-press | duration-xs (160)ease-exit | Hides at once |
| AVT9 | hover the add button | add button | background-color, border-color, icon color | bg-primary, border-primary, fg-quaternary → bg-primary_hover, border-primary, fg-quaternary_hover | duration-sm (200)ease-out | Swaps at once |
| AVT10 | keyboard focus on the add button or a linked avatar | focus ring | box-shadow | none → focus ring (2px bg-primary gap, 4px focus-ring) | duration-2xs (120)ease-out | Shows at once |
Accessibility
- With a photo, the image has alt set to alt or name; with no photo, the root has role img and aria-label, and the initials or icon are aria-hidden.
- Status is part of the name, not color only: for example "Olivia Rhye, online" or "Olivia Rhye, verified".
- A count badge adds its number to the name, for example "Olivia Rhye, 3 new"; the caller sets the words with the badge slot when the count means something else.
- AvatarGroup is a list (ul and li) with aria-label groupLabel; hidden people are in the +n item, named "5 more people".
- The add button is a native button named "Add person"; xs (24px) and larger meet the 24 x 24px target (WCAG 2.5.8).
- The hover lift is decoration only; avatars that are links or buttons show the focus ring on keyboard focus.
- Reduced motion: the photo and fallback show at once and nothing lifts or scales.