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.json

The source is copied into components/panelui/, where you can edit it.

npm install panelui-web

Import 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-variants

Copy 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. status draws a presence dot: online, away, busy, or a hollow offline. For an icon, such as a check on a verified account, pass the icon as children and a color. label is what a screen reader says after the name.
  • Avatar.Group — A row of avatars overlapping one another. It sets the size and shape of every avatar in it. Hovered or focused, it opens up so each face can be seen whole. Past max, 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

PropTypeDefaultDescription
srcstring—The picture. Without it, or until it has loaded, the initials show.
namestring—Who it is. Gives the initials, the colour and what a screen reader says.
altstring—Read out in place of name, when that is not the right thing to say.
sizeAvatarSize—Default md. In a group, the group's size.
shapeAvatarShapecirclecircle for people, square for teams, projects and things. Default circle.
colorAvatarColor—The tint behind the initials. Picked from name when left out.
fallbackReactNode—In place of the initials, such as an icon for a team or a bot.

Avatar.Badge

PropTypeDefaultDescription
statusAvatarStatus—A presence dot in the colour that says it. Leave out for a badge of your own.
colorAvatarColor—For a badge with an icon in it: the fill behind the icon.
labelstring—What a screen reader says after the name. Defaults to the status.

Avatar.Group

PropTypeDefaultDescription
maxnumber—How many to show. The rest are counted in a last circle that lists them.
sizeAvatarSize—The size of every avatar in it. Default md.
shapeAvatarShapecircleThe shape of every avatar in it. Default circle.
labelstring—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"

On this page