Components
Avatar
Displays a user's profile image inside a circle, gracefully falling back to initials or an icon when the image is missing or fails to load.
Basic usage
Compose an Avatar from an AvatarImage and an AvatarFallback. Radix shows the image once it loads and the fallback otherwise.
Image & fallback
The fallback is automatic — when AvatarImage is absent or its src fails to load, the initials render in its place on bg-muted. Recolour it with a role token pair such as bg-accent text-accent-foreground, never a raw scale step.
Sizes
The avatar is size-8 by default. Override it with any Tailwind size-* utility — the image and fallback scale to fill the circle.
Letter avatar
LetterAvatar is the one you reach for in lists: give it a name and it renders the photo if there is one, otherwise Vietnamese-style initials (first letter of the family name plus first letter of the given name) on a tone derived from that name. It deliberately avoids the Radix primitive — no image-loading state means no client island, so 200 rows render straight on the server.
Deterministic tones
The tone is a pure function of the name, so the same person is the same colour in every Comitor app — and the same colour again in canvas, e-mail, and PDF output, because getAvatarToneColors(name, mode) from @comitor/ui/tokens reads the same tone table. Use getAvatarToneClasses(name) when you need those classes on your own element; do not rebuild the hash, that is the shortest path to one person having two colours.
Avatar group
AvatarGroup stacks members and collapses the rest into a “+k” chip. Every face is a focusable button carrying a tooltip with the full name and an optional caption — overlapping avatars hide part of the initials, so without the tooltip the stack is decoration. Pass label and overflowLabel explicitly: the group's default aria-label is the Vietnamese “Thành viên”, and the “+k” button's own sr-only name is Vietnamese too.
Group surface
Tones are alpha tints, and the group intentionally overlaps them. Without an opaque layer beneath, the tint of the upper avatar composites with the colour of the one below instead of with the page. surfaceClassName supplies both that layer and the separating ring, and defaults to bg-card ring-card. Standing the group on another surface means restating both halves.
Props
Avatar
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Tailwind classes merged onto the root (size-8 rounded-full overflow-hidden). Override size with size-* and the shape stays a circle. |
...rest | AvatarPrimitive.Root props | — | All Radix Avatar Root props are forwarded to the wrapper element, which carries data-slot="avatar". |
AvatarImage
| Prop | Type | Default | Description |
|---|---|---|---|
srcrequired | string | — | URL of the image to display. |
altrequired | string | — | Alternative text describing the person; required for screen readers. |
onLoadingStatusChange | (status: 'idle' | 'loading' | 'loaded' | 'error') => void | — | Fires as the image moves through its loading lifecycle. |
className | string | — | Merged onto the img, which is aspect-square size-full by default. |
AvatarFallback
| Prop | Type | Default | Description |
|---|---|---|---|
delayMs | number | — | Wait this many milliseconds before rendering the fallback, avoiding a flash while a fast image loads. Renders immediately when omitted. |
childrenrequired | React.ReactNode | — | Content shown when the image is absent or fails — typically initials or an icon. |
className | string | — | Merged onto the fallback, which fills the circle on bg-muted by default. |
LetterAvatar
| Prop | Type | Default | Description |
|---|---|---|---|
namerequired | string | — | Full name — drives both the initials and the tone. Also rendered into an sr-only span so the avatar is never a mute coloured square. |
src | string | null | — | Photo URL. When present the image is shown instead of the initials, and the tone class is dropped. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | One of five fixed sizes: 24, 28, 36, 44, and 56 px, each with a matching type scale. |
square | boolean | false | Rounded square instead of a circle — the house convention for a workspace or an app, not a person. |
toneClassName | string | — | Overrides the name-derived tone with your own token classes. Use it for a deliberate meaning (an app accent), never to re-roll the colour of a person. |
...rest | React.ComponentProps<'span'> | — | All native span attributes are forwarded; children are not accepted. |
AvatarGroup
| Prop | Type | Default | Description |
|---|---|---|---|
itemsrequired | AvatarGroupItem[] | — | Members to show: { id, name, src?, caption? }. caption becomes the tooltip’s second line — an email or a role. |
max | number | 4 | How many avatars render before the rest collapse into the "+k" chip. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'sm' | Passed through to each LetterAvatar. The overlap scales with it — roughly 20% at every size, so the initials stay readable. |
onOverflowClick | () => void | — | Makes the "+k" chip a button — typically opens the full member list. Without it the chip renders as static text. |
overflowLabel | (count: number) => string | (count) => `Xem tất cả ${count} thành viên` | sr-only name of the "+k" button when onOverflowClick makes it clickable. A function, not a template string, and it receives items.length — the whole group, not the hidden remainder. Vietnamese by default: translating only label leaves this button speaking Vietnamese. |
label | string | 'Thành viên' | aria-label for the group. The package default is Vietnamese — pass an English string in an English UI. |
surfaceClassName | string | 'bg-card ring-card' | Background plus ring colour of the surface the group sits on. Both halves must be restated together when the group is not on a card. |
className | string | — | Merged onto the group wrapper, which carries role="group" and data-slot="avatar-group". |
Accessibility
- Always pass a descriptive
altonAvatarImage— typically the person's name — so screen readers announce who is shown. - Radix unmounts the
<img>entirely unless it has loaded, so thealttext disappears with it. A fallback of bare initials has no accessible name of its own — put the person's name in the surrounding link or list row, or useLetterAvatar, which carries it in ansr-onlyspan. - Radix only swaps in the fallback after the image errors or while it loads, preventing layout shift and a flash of empty space.
- Use
delayMson the fallback to avoid a brief initials flicker when the image loads quickly over a fast connection. LetterAvatarcarries the name in ansr-onlyspan on both branches — the initials themselves arearia-hidden, since “NT” read aloud means nothing.- Each face in
AvatarGroupis a real<button>so the tooltip is reachable by keyboard. Its focus ring usesoutline-foregroundrather than the gold--ring, which would sit below the 3:1 that WCAG 2.4.11 asks of a focus indicator on the light palette. - A bare avatar is presentational — when it acts as a control, wrap it in a
<button>or link with its own accessible label.