---
title: "Avatar"
description: "A picture or initials standing for a person, with the person's name as its accessible label and a neutral fallback when there is no picture."
url: "https://opsinjs.pensievelabs.org/components/avatar"
source: "https://opsinjs.pensievelabs.org/components/avatar.md"
section: "Components"
status: "shipped"
kind: "component"
category: "utility"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["profile picture", "initials", "user image"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="avatar"
  status="shipped"
  questions="[
  &#x22;No contrast pair has been measured for the initials ink or the person glyph on the muted fill this component draws.&#x22;,
  &#x22;Nobody has listened to it in a screen reader, so whether the name reads cleanly through role=\&#x22;img\&#x22; is unverified.&#x22;,
  &#x22;Initials are taken by code point, not grapheme cluster, so a first character with a combining mark can lose the mark.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="avatar" />

## Installation [#installation]

<ComponentInstall name="avatar" unbuilt="false" importPath="@/components/ui/avatar" dependencies="[&#x22;lucide-react&#x22;]" />

## Usage [#usage]

```tsx
import { Avatar } from "@/components/ui/avatar"
```

```tsx
<Avatar name="Ada Vance" src="/people/ada-vance.jpg" size="md" />
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;Beside a person's name in a message, a comment or a care-team list, to tell one author from another.&#x22;,
  &#x22;As the fallback identity for a person with no photo, where initials or a neutral glyph stand in without pretending a picture exists.&#x22;,
  &#x22;At a small fixed size in a dense list, where a full name would not fit.&#x22;,
]"
  avoid="[
  { case: &#x22;You want the mark to open a menu or a profile when it is pressed.&#x22;, instead: &#x22;icon-button&#x22; },
  { case: &#x22;You are showing a count or a short status word rather than a person.&#x22;, instead: &#x22;badge&#x22; },
  { case: &#x22;You need a round placeholder to hold an avatar's space while its picture is still loading.&#x22;, instead: &#x22;skeleton&#x22; },
  { case: &#x22;You are trying to show who wrote a piece of health guidance and on whose authority.&#x22;, instead: &#x22;source-citation&#x22; },
]"
/>

## Anatomy [#anatomy]

<Anatomy
  name="avatar"
  parts="[
  {
    name: &#x22;Avatar&#x22;,
    describes: &#x22;The root: a neutral circle sized in rem. Carries the name as its label when a fallback shows. Never a control.&#x22;,
    prop: &#x22;size&#x22;,
  },
  {
    name: &#x22;Avatar.Image&#x22;,
    describes: &#x22;The picture, when supplied and loaded. Its alt is the name. On error it is dropped for the fallback.&#x22;,
    prop: &#x22;src&#x22;,
  },
  {
    name: &#x22;Avatar.Fallback&#x22;,
    describes: &#x22;Initials from the name, or a lucide User glyph when the name yields none. Hidden from assistive technology.&#x22;,
    prop: &#x22;name&#x22;,
  },
]"
/>

## Examples [#examples]

### A name becomes its initials [#a-name-becomes-its-initials]

`name` drives the initials. A two-part name gives first and last, a single name
gives one, and a non-Latin name keeps its own script rather than a transliterated guess.

<ComponentPreview name="avatar-initials-from-a-name" kind="example" align="start" />

### The fallback chain, one tier at a time [#the-fallback-chain-one-tier-at-a-time]

`src` and `name` decide the tier: a picture that loads, initials when there is no
picture, and a neutral person glyph when there is neither.

<ComponentPreview name="avatar-the-fallback-chain" kind="example" align="start" />

## Content guidelines [#content-guidelines]

Pass the person's real name, spelled the way they spell it, through `name`. It is
the alt text and the source of the initials at once, so a placeholder like "User"
becomes a wrong label and wrong initials. opsinjs ships no default face and does not
moderate a `src`.

<DoDont>
  <DoDont.Do>
    **"Ada Vance", with her photo, or "AV" when there is none.** One field feeds the alt text and the initials.
  </DoDont.Do>

  <DoDont.Dont>
    **A generic silhouette used for everyone, with no name behind it.** A nameless circle identifies nobody.
  </DoDont.Dont>
</DoDont>

## Accessibility [#accessibility]

**Checked by `pnpm run check:a11y` on every commit.** Every colour is a role
token, no type size is in `px`, and no banned word appears anywhere.

**Audited against WCAG 2.2 AA**, in a source pass over the component code and a
rendered pass with axe-core, target-size, focus-visible and reflow checks. The audit
is author-run, not an independent review, and clinical review is still pending.

The audit found one defect worth fixing: a picture with a missing or blank name
rendered with an empty `alt`, so a screen reader announced nothing while a sighted
reader still saw a face. The picture tier now requires a usable name too, so a
nameless avatar falls through to the neutral person glyph instead, matching the
fallback chain this page already promises.

* The avatar takes no focus and is not a tab stop. Inside a button or a link, that
  control owns the focus and the name.
* With a fallback showing, the root is `role="img"` with the name as `aria-label`,
  and the initials or glyph are `aria-hidden`.
* With a picture showing, the image's own `alt` carries the name and the root stays a
  plain container, so the name is announced once.
* An empty name warns in development, draws the person glyph and leaves the circle
  unlabelled, because the component cannot invent a label.

**Left open by the audit.** Contrast of the initials and the glyph on the muted fill
comes from shared role tokens, not a literal colour here; the rendered pass found no
violation, but no ContrastReport data exists yet for this pair. Initials are still
taken by code point rather than grapheme cluster, so a combining mark can be lost, as
the notice above already states.

<KeyboardTable
  name="avatar"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Skips the avatar&#x22;,
    notes: &#x22;Not focusable. Inside a button or link, the wrapper is the tab stop.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Skips the avatar in reverse&#x22;,
    notes: &#x22;Absent from the tab sequence in both directions.&#x22;,
  },
  {
    keys: &#x22;Enter&#x22;,
    action: &#x22;Does nothing here&#x22;,
    notes: &#x22;The avatar captures no key. A wrapping link or button owns Enter.&#x22;,
  },
]"
/>

<ContrastReport component="avatar" />

## API reference [#api-reference]

<PropsTable name="AvatarProps" />

`name` is required, but an empty string still compiles. It warns in development and
falls back to the person glyph. A missing or broken `src` is a fallback, not an error.
`size` defaults to `md`, and an unknown value is repaired to `md` with a warning.

## Related [#related]

* [IconButton](./icon-button.mdx) is a control you press. An avatar presents a person and does nothing when tapped.
* [Badge](./badge.mdx) is a count or a short status word, not a person.
* [Skeleton](./skeleton.mdx) holds an avatar's space while a picture loads. An avatar is what arrives once it has.
