Avatar
A person or a team as a picture, or their initials in a colour of their own, with a badge and a group that opens up.
import { Avatar } from '@/components/panelui/avatar';
const ME = 'https://avatars.githubusercontent.com/u/127331761?v=4';
export default function AvatarOverview() {
return (
<div className="flex flex-wrap items-center justify-center gap-8">
<Avatar src={ME} name="Khalid Abdi" size="xl">
<Avatar.Badge status="online" />
</Avatar>
<Avatar name="Noor Haddad" size="xl" />
<Avatar name="Harbour Works" shape="square" size="xl" />
<Avatar.Group size="lg" max={4} label="Editors">
<Avatar src={ME} name="Khalid Abdi" />
<Avatar name="Ines Duarte" />
<Avatar name="Kofi Mensah" />
<Avatar name="Yuki Tanabe" />
<Avatar name="Ravi Patel" />
<Avatar name="Selma Berg" />
</Avatar.Group>
</div>
);
}Use it to show who someone is: the author of a comment, the people on a team, who has read a document. It shows src once the picture has loaded, and until then, or if it never loads, the initials of name.
The initials sit on a tint of one of the theme's chart colours, picked from the name. The same person gets the same colour everywhere, so a list of people without pictures can still be told apart.
A badge and an overlapping avatar are cut out of the avatar beneath them, not drawn with a ring. The gap shows whatever the avatar sits on, so it works on a card, a tinted row or a photograph without a colour to match.
name is also what a screen reader says, with the badge's status after it. Pass alt when something else should be said.
Installation
npx shadcn@latest add https://panelui.dev/r/web/avatar.jsonpnpm dlx shadcn@latest add https://panelui.dev/r/web/avatar.jsonyarn dlx shadcn@latest add https://panelui.dev/r/web/avatar.jsonbunx shadcn@latest add https://panelui.dev/r/web/avatar.jsonThe source is copied into components/panelui/, where you can edit it.
npm install panelui-webpnpm add panelui-webyarn add panelui-webbun add panelui-webImport from panelui-web/avatar instead of @/components/panelui/avatar. Tailwind needs one line to read the package's classes, which is on the installation page.
Install the packages it uses:
npm install @base-ui/react tailwind-variantspnpm add @base-ui/react tailwind-variantsyarn add @base-ui/react tailwind-variantsbun add @base-ui/react tailwind-variantsCopy the source into components/panelui/avatar.tsx:
Usage
import { Avatar } from "@/components/panelui/avatar"<Avatar src="/photos/khalid.jpg" name="Khalid Abdi" />
<Avatar name="Kofi Mensah" size="lg">
<Avatar.Badge status="online" />
</Avatar>
<Avatar.Group max={4} label="Read by">
{people.map((person) => (
<Avatar key={person.id} src={person.photo} name={person.name} />
))}
</Avatar.Group>Composition
<Avatar>
<Avatar.Badge />
</Avatar>
<Avatar.Group>
<Avatar />
</Avatar.Group>Avatar.Badge— A dot or a small icon on the avatar's lower trailing edge.statusdraws a presence dot:online,away,busy, or a hollowoffline. For an icon, such as a check on a verified account, pass the icon as children and acolor.labelis what a screen reader says after the name.Avatar.Group— A row of avatars overlapping one another. It sets thesizeandshapeof every avatar in it. Hovered or focused, it opens up so each face can be seen whole. Pastmax, the rest are counted in a last circle that opens a list of their names.
Examples
Sizes and shapes
xs, sm, md, lg and xl. circle is for people. square is for teams, projects and things, with corners that grow with the size.
import { Avatar, type AvatarSize } from '@/components/panelui/avatar';
const ME = 'https://avatars.githubusercontent.com/u/127331761?v=4';
const SIZES: AvatarSize[] = ['xs', 'sm', 'md', 'lg', 'xl'];
export default function AvatarSizes() {
return (
<div className="flex flex-col items-center gap-6">
<div className="flex items-end gap-4">
{SIZES.map((size) => (
<Avatar key={size} size={size} name="Khalid Abdi" src={ME} />
))}
</div>
<div className="flex items-end gap-4">
{SIZES.map((size) => (
<Avatar key={size} size={size} shape="square" name="Harbour Works" />
))}
</div>
</div>
);
}Without a picture
Each name lands on a colour of its own. A picture that fails to load falls back to the initials. fallback replaces the initials with something else, such as an icon for a bot, and with no name at all the avatar shows a person's outline.
import { BotIcon } from 'lucide-react';
import { Avatar } from '@/components/panelui/avatar';
const NAMES = ['Aiko Mori', 'Bram de Vries', 'Chiara Rossi', 'Dmitri Volkov', 'Esi Owusu', 'Farah Nasser'];
export default function AvatarFallback() {
return (
<div className="flex flex-col items-center gap-6">
{/* Each name lands on its own colour, the same one every time. */}
<div className="flex flex-wrap justify-center gap-3">
{NAMES.map((name) => (
<Avatar key={name} name={name} size="lg" />
))}
</div>
<div className="flex items-center gap-3">
{/* A picture that fails to load shows the initials instead. */}
<Avatar name="Gil Amsalem" src="https://example.invalid/gil.jpg" size="lg" />
{/* Something that is not a person. */}
<Avatar alt="Release bot" fallback={<BotIcon />} color="chart-2" shape="square" size="lg" />
{/* No name at all. */}
<Avatar alt="Guest" size="lg" />
</div>
</div>
);
}Badges
status draws a presence dot, and offline is hollow. A badge can hold an icon instead, such as a check on a verified account. The gap round a badge is cut through the avatar, not painted, so it shows whatever the avatar sits on.
import { BadgeCheckIcon } from 'lucide-react';
import { Avatar } from '@/components/panelui/avatar';
const ME = 'https://avatars.githubusercontent.com/u/127331761?v=4';
export default function AvatarBadges() {
return (
<div className="flex flex-wrap items-center justify-center gap-5">
<Avatar name="Khalid Abdi" src={ME} size="lg">
<Avatar.Badge status="online" />
</Avatar>
<Avatar name="Omar Haddad" size="lg">
<Avatar.Badge status="away" />
</Avatar>
<Avatar name="Pia Lund" size="lg">
<Avatar.Badge status="busy" />
</Avatar>
<Avatar name="Quentin Roy" size="lg">
<Avatar.Badge status="offline" />
</Avatar>
<Avatar name="Harbour Works" shape="square" size="lg">
<Avatar.Badge color="chart-2" label="verified">
<BadgeCheckIcon />
</Avatar.Badge>
</Avatar>
</div>
);
}Groups
Hover over a group, or move focus into it, and it opens up. Press the count to see who it stands for. In a group of square avatars, each is cut round the shape of the one over it.
'use client';
import { Avatar } from '@/components/panelui/avatar';
const ME = 'https://avatars.githubusercontent.com/u/127331761?v=4';
const READERS = ['Tomás Ferreira', 'Ama Boateng', 'Lea Schmidt', 'Nikhil Rao', 'Olga Petrova', 'Sami Laine'];
const TEAMS = ['Design', 'Platform', 'Growth', 'Support', 'Finance'];
export default function AvatarGroupDemo() {
return (
<div className="flex flex-col items-center gap-8">
{/* Hover or tab into it and it opens up. Press the count to see the rest. */}
<Avatar.Group max={4} size="lg" label="Read by">
<Avatar name="Khalid Abdi" src={ME} />
{READERS.map((name) => (
<Avatar key={name} name={name} />
))}
</Avatar.Group>
<Avatar.Group shape="square" label="Teams">
{TEAMS.map((team) => (
<Avatar key={team} name={team} />
))}
</Avatar.Group>
</div>
);
}API Reference
Avatar
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | — | The picture. Without it, or until it has loaded, the initials show. |
name | string | — | Who it is. Gives the initials, the colour and what a screen reader says. |
alt | string | — | Read out in place of name, when that is not the right thing to say. |
size | AvatarSize | — | Default md. In a group, the group's size. |
shape | AvatarShape | circle | circle for people, square for teams, projects and things. Default circle. |
color | AvatarColor | — | The tint behind the initials. Picked from name when left out. |
fallback | ReactNode | — | In place of the initials, such as an icon for a team or a bot. |
Avatar.Badge
| Prop | Type | Default | Description |
|---|---|---|---|
status | AvatarStatus | — | A presence dot in the colour that says it. Leave out for a badge of your own. |
color | AvatarColor | — | For a badge with an icon in it: the fill behind the icon. |
label | string | — | What a screen reader says after the name. Defaults to the status. |
Avatar.Group
| Prop | Type | Default | Description |
|---|---|---|---|
max | number | — | How many to show. The rest are counted in a last circle that lists them. |
size | AvatarSize | — | The size of every avatar in it. Default md. |
shape | AvatarShape | circle | The shape of every avatar in it. Default circle. |
label | string | — | What a screen reader calls the group, such as "Members". |
Every part also takes the props of the element it renders, and a className.
Notes
Loading
The initials show from the start and the picture replaces them once it has loaded, so nothing flickers between an empty circle and a face.
Reduced motion
With reduced motion turned on, a group does not open up when hovered.
In a group
A group is a role="group". Give it a label that says who the people are, such as "Members" or "Read by".
Server Components
Avatar is a client component. In a file that starts with "use client", write its parts with a dot, as on this page. A Server Component cannot read Avatar.Badge off the import, so each part is also exported by name: AvatarBadge is Avatar.Badge, and so on for the others.
import { Avatar, AvatarBadge, AvatarGroup } from "@/components/panelui/avatar"