opsinjs
ComponentsUtility

Avatar

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.

Preview

Theme
Density
Text
Status
avatar · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/avatar

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { Avatar } from "@/components/ui/avatar"
<Avatar name="Ada Vance" src="/people/ada-vance.jpg" size="md" />

When to use it

Use it when

  • Beside a person's name in a message, a comment or a care-team list, to tell one author from another.
  • As the fallback identity for a person with no photo, where initials or a neutral glyph stand in without pretending a picture exists.
  • At a small fixed size in a dense list, where a full name would not fit.

Do not use it when

  • You want the mark to open a menu or a profile when it is pressed.Use icon-button instead.
  • You are showing a count or a short status word rather than a person.Use badge instead.
  • You need a round placeholder to hold an avatar's space while its picture is still loading.Use skeleton instead.
  • You are trying to show who wrote a piece of health guidance and on whose authority.Use source-citation instead.

Anatomy

  1. AvatarThe root: a neutral circle sized in rem. Carries the name as its label when a fallback shows. Never a control.Controlled by size
  2. Avatar.ImageThe picture, when supplied and loaded. Its alt is the name. On error it is dropped for the fallback.Controlled by src
  3. Avatar.FallbackInitials from the name, or a lucide User glyph when the name yields none. Hidden from assistive technology.Controlled by name

Examples

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.

Theme
Density
Text
Status
avatar-initials-from-a-name · base base · style base-lyraOpen under the product theme

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.

Theme
Density
Text
Status
avatar-the-fallback-chain · base base · style base-lyraOpen under the product theme

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.

Do

"Ada Vance", with her photo, or "AV" when there is none. One field feeds the alt text and the initials.

Don’t

A generic silhouette used for everyone, with no name behind it. A nameless circle identifies nobody.

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.

KeyActionNotes
TabSkips the avatarNot focusable. Inside a button or link, the wrapper is the tab stop.
Shift+TabSkips the avatar in reverseAbsent from the tab sequence in both directions.
EnterDoes nothing hereThe avatar captures no key. A wrapping link or button owns Enter.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

API reference

Prop

Type

Generated from AvatarProps in registry/bases/base/avatar.tsx.

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.

  • IconButton is a control you press. An avatar presents a person and does nothing when tapped.
  • Badge is a count or a short status word, not a person.
  • Skeleton holds an avatar's space while a picture loads. An avatar is what arrives once it has.

On this page