Avatar
An avatar that shows initials until the image loads and then crossfades, with a breathing presence dot, a hover ring that expands, five sizes and two shapes.
The avatar you need for member lists, comment threads and account menus. The image preloads after mount; until then the avatar shows initials derived from alt — the last two characters of a Chinese name, or the first letters of the first two words of a Latin name — on a background picked from six neutral warm-grey tones by a deterministic hash of the name, so the same person always gets the same tone. When the image finishes loading the two layers crossfade; if it fails, the initials stay. A presence dot can sit in the bottom-right corner and breathes slowly when online. With ring on, hovering the avatar or focusing a wrapping link expands an outline out from its edge.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add avatar.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<Avatar/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/avatar.en.jsonOr, once registries are configured in components.json, install it as @webberui/avatar.
Usage
import { Avatar } from "@/components/ui/avatar";
<Avatar src="/team/yuqing.jpg" alt="林雨晴" status="online" ring />
{/* No image: shows "雨晴" */}
<Avatar alt="林雨晴" size="lg" shape="rounded" />
{/* Inside a link, focus shows the ring too */}
<a href="/members/yuqing" className="rounded-full focus-visible:outline-none">
<Avatar src="/team/yuqing.jpg" alt="林雨晴" ring delayMs={300} />
</a>Props
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | — | Image URL; the initials show when omitted or when loading fails |
alt | string | — | Required. Alt text for the image, and the source of the initials and background tone |
fallback | ReactNode | — | Overrides the generated initials; accepts any node |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | 24 / 32 / 40 / 56 / 72px |
shape | "circle" | "rounded" | "circle" | A circle or a square with 22% rounded corners |
status | "online" | "busy" | "away" | "offline" | — | Presence dot in the bottom-right corner; hidden when omitted |
statusLabel | string | By status: "Online" / "Busy" / "Away" / "Offline" | Status text for screen readers |
ring | boolean | false | Shows an expanding outline on hover or when a wrapper is focused |
duration | number | 200 | Crossfade and ring transition duration (ms), written to --wb-avatar-duration |
delayMs | number | 0 | How long to wait before showing the initials when src is set (ms); 300 avoids a flash of initials when images usually hit the cache |
onLoadingStatusChange | (status: "idle" | "loading" | "loaded" | "error") => void | — | Called whenever the image loading status changes |
className | string | — | Passed to the outermost element |
getInitials(alt) is also a named export, so other places (a text avatar in a dropdown, say) can reuse the same initials rule.
How it works
- Initials rule: names containing CJK ideographs use the last two ideographs ("林雨晴" → "雨晴"; one for a single-character name), because Chinese speakers address people by given name; other names use the uppercased first letters of the first two words ("Maya Lin" → "ML").
- Preloading runs with
new Image()only after mount. The server render always outputs the initials layer, matching the first client render, so there is no hydration warning. - The image layer and the initials layer are absolutely positioned in the same box, so the layout does not jump during the crossfade.
- The presence dot is 25% of the avatar, with a 2px outer ring in the background colour. Away is drawn as a crescent with a CSS
mask, so the cut-out is truly transparent; offline is a hollow circle. Only online pulses (the halo scales to 1.8× and fades out, every 2 seconds). - Timing is exposed as
--wb-avatar-duration(default 200ms) and--wb-ease-out, both overridable in global styles.
Accessibility
- The image uses
alt; before it loads, the initials layer isrole="img"with the full name as itsaria-label - With a
status, sr-only text such as "(Online)" is appended, so screen readers announce "Maya Lin (Online)"; the dot and the pulse arearia-hidden - The ring responds to hover and to
focus-visibleon a wrapping link or button, so keyboard users can see where focus is - When the user's system has reduce motion turned on, the crossfade becomes an instant swap and the online dot does not pulse
Status Badge
A badge with a count that rolls digit by digit, a removable mode that shrinks out while neighbours slide into place, and a pulsing variant for live states.
Keycap Shortcut
A React component for keycap-styled shortcut labels — combine multiple keys, mod shows ⌘ or Ctrl per platform, and with listening enabled each keycap presses down and springs back when the user actually hits the matching key, with a ring flash once the whole combo is held.