Welcome to Oshon · v1.0  ·  Now in public beta for enterprise teams Read the launch notes
Data DisplayUpdatedFreeWCAG 2.2 AA

Avatar

Image / initials · online / offline / none · 5 sizes · group with +N overflow.

Preview

Live preview
@oshon-ai/components
Real AI headshots
Initials — name-hash brand tints
Status — online / offline / none
online
offline
none
Sizes — xs · s · m · l · mobile
Shape — circle (default) + square
AvatarGroup — overlapping stack with +N overflow
+2
Editable — click the camera badge to upload your own image
Click the camera badgeJPEG / PNG / WebP / GIF · ≤ 5 MB

Installation

Install the runtime packages:

pnpm
pnpm add @oshon-ai/components @oshon-ai/tokens @oshon-ai/primitives

Or scaffold the component source directly into your codebase (shadcn-style):

pnpm
pnpm dlx @oshon-ai/cli add avatar

Wire the tokens into your Tailwind v4 stylesheet:

css
/* app/globals.css */
@import 'tailwindcss';
@import '@oshon-ai/tokens/css';
@import '@oshon-ai/tokens/tailwind';

New here? Walk through the full setup — prereqs, theming, your first render.

Usage

Import the component and render it. Every component supports the standard tier, size, and disabled props where applicable.

tsx
'use client';
import { Avatar } from '@oshon-ai/components';

export default function Example() {
  return <Avatar />;
}

Styling

Three layers of customization, in order of escape-hatch strength: className overrides → data-attribute targeting → CSS custom properties.

Passing Tailwind classes

Every Oshon component accepts a className prop merged AFTER the component's default classes. Use it to override spacing, color, or size without forking the component.

tsx
<Avatar
  className="ring-2 ring-offset-2 ring-blue-500"
/>

Data attributes

Oshon components expose their internal state as data-oshon-* attributes so you can target them from CSS without coupling to internal class names. The most common attributes are listed below — see the component's source for the full set.

AttributeValuesDescription
data-oshon-sizexs · s · m · l · mobileVisual size axis. Mirrors the `size` prop.
data-oshon-tierprimary · secondary · tertiaryVisual emphasis tier (Button family). Mirrors the `tier` prop.
data-oshon-stateenabled · active · error · disabledComponent surface state. Set automatically based on props.
data-disabledtrue · (omitted)Set when `disabled` is true. Pair with `:disabled` CSS for native input components.
data-stateopen · closed · checked · unchecked · …Radix-derived state for overlay components (Dialog, Tabs, Toggle, etc.).
css
/* Target the secondary tier specifically */
[data-oshon-tier="secondary"] {
  --oshon-color-primary-700: var(--my-brand-color);
}

Interactive states

Every interactive component supports the standard CSS pseudo- classes plus Tailwind's state variants. Focus rings always use :focus-visible so keyboard users see them but mouse users don't.

  • :hover / hover:* — pointer hover
  • :focus-visible / focus-visible:* — keyboard focus
  • :active / active:* — pressed
  • :disabled / disabled:* — set via the disabled prop

Anatomy

The named regions a consumer composes when rendering this component. Each is documented separately so you can target keyboard nav, ARIA labels, and slot props with precision.

src + alt

Image source URL + alt text. Alt is strongly recommended; when omitted, the avatar uses `name` as the accessible label.

name

Display name. Used for (1) initials derivation, (2) initials background-color hash seed, (3) accessible-label fallback when `alt` is omitted.

initials

Override the auto-derived initials — useful for non-Latin names, two-letter handles ("OS"), or single-character marks.

fallback

Custom ReactNode fallback. Wins over both `initials` and the auto-derived value.

status / statusLabel

Bottom-right presence dot. `"online"` (success-500) / `"offline"` (on-surface-muted) / `"none"` (default — suppressed). `statusLabel` overrides the auto-generated aria-label.

Keyboard

Avatar is a non-interactive presentational element by default — it does not enter the tab order. Wrap in a `<button>` or `<a>` if a click target is needed; the avatar will then inherit that element's keyboard contract. The presence dot is announced as `role="status"` with an accessible name from `statusLabel` (default: the status string).

Accessibility

Every Oshon component ships axe-clean. We test in CI on every PR and publish the audit log per component.

WCAG level
2.2 AA
Screen readers tested
VoiceOver (macOS), NVDA (Windows)
Last axe audit
2026-05-19

Do / Don't

✓ Do

Image with name fallback
<Avatar src={user.photo} alt={user.name} name={user.name} />
Initials only
<Avatar name="Ada Lovelace" />
With presence dot
<Avatar src={user.photo} name={user.name} status="online" />
Square shape
<Avatar src={team.logo} name={team.name} shape="square" />

✗ Don't

Setting src without a name or alt
<Avatar src="..." />

Both `name` (for the initials fallback when the image fails to load) and `alt` (for screen readers) need to be set. Pass `name` at minimum — the avatar will use it as the `<img>` alt when `alt` is omitted.

Hand-rendering initials inline
<Avatar fallback={<span>AL</span>} name="Ada Lovelace" />

The component already derives `AL` from `name` and applies the brand-soft background hash. Passing the same value as `fallback` bypasses the color hash so the avatar looks generic. Drop the fallback and let the auto-derivation run.

Putting Avatars inside AvatarGroup with explicit sizes
<AvatarGroup size="l">
  <Avatar size="s" name="Ada" />
  <Avatar size="s" name="Ben" />
</AvatarGroup>

AvatarGroup propagates `size` to children that omit it. Overriding each child fights the group's overlap math (the negative-margin step is sized for the group's `size`, not each child) — the stack stops reading as a unit.

Design rationale

Initials are the default state, not an afterthought — most production apps render avatars from a user record where `src` may not be set, may 404, or may be a Cloudinary URL that takes a few hundred ms to load. The brand-soft hash palette gives each unique name a stable color signature so a list of teammates reads as differentiated identity tiles instead of a wall of greys. AvatarGroup overlaps siblings by 1/3 the frame width (the standard Material/Carbon convention) with a 2-px surface-raised ring around each item so the stack reads cleanly against any backdrop. The component is presentational by default — wrap in a button or anchor at the call site to make it interactive, so the avatar doesn't fight the surrounding control's keyboard contract.