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
avatar · base base · style base-lyraOpen under the product themeInstallation
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-buttoninstead. - You are showing a count or a short status word rather than a person.Use
badgeinstead. - You need a round placeholder to hold an avatar's space while its picture is still loading.Use
skeletoninstead. - You are trying to show who wrote a piece of health guidance and on whose authority.Use
source-citationinstead.
Anatomy
AvatarThe root: a neutral circle sized in rem. Carries the name as its label when a fallback shows. Never a control.Controlled bysizeAvatar.ImageThe picture, when supplied and loaded. Its alt is the name. On error it is dropped for the fallback.Controlled bysrcAvatar.FallbackInitials from the name, or a lucide User glyph when the name yields none. Hidden from assistive technology.Controlled byname
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.
avatar-initials-from-a-name · base base · style base-lyraOpen under the product themeThe 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.
avatar-the-fallback-chain · base base · style base-lyraOpen under the product themeContent 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 asaria-label, and the initials or glyph arearia-hidden. - With a picture showing, the image's own
altcarries 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.
| Key | Action | Notes |
|---|---|---|
| Tab | Skips the avatar | Not focusable. Inside a button or link, the wrapper is the tab stop. |
| Shift+Tab | Skips the avatar in reverse | Absent from the tab sequence in both directions. |
| Enter | Does nothing here | The avatar captures no key. A wrapping link or button owns Enter. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below 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.
Related
- 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.