Skip to contentVibraUI
Data display

Avatar

A round picture of a person, with initials while it loads or when there is none.

shadcn's base-nova avatar with one change: AvatarBadge sits on the inline end, so a presence dot crosses to the bottom-left corner in a right-to-left page. Otherwise it takes the kit's look from the theme alone — the fallback on the muted plane, the badge on --primary, which every Vibra palette points at the accent. Name an avatar once, with aria-label on its root, and hide its picture and initials, or a screen reader hears the initials while the face loads; given a name, the root — and a badge given one — is role="img" by itself, because aria-label on a span with no role is prohibited and screen readers drop it. The kit draws its people with avatarFor(name) faces, and AvatarGroup (the kit item) adds names and an overflow count.

Install

npx shadcn@latest add @vibra/avatar

Needs the @vibra registry in your components.json — set it up once.

Examples

Fallbacks

The picture, the initials while it loads or when there is none, and a glyph when there is no name either.

Presence

A dot on the corner and the word under it; do-not-disturb adds a bar and offline is hollow, so no two differ by colour alone.

With an icon badge

AvatarBadge carrying a tick for the approvers who have signed off, each avatar named with its state.

Unread count

A count on the top corner of a conversation's face; the row says the number in words.

Stacks at each size

AvatarGroup at sm, default and lg; the overflow count takes the size of the faces beside it.

Stack with a caption

Faces beside the sentence that names them, hidden from a screen reader so they are not read twice.

Stack that opens the list

The whole stack as one button, and a popover with every name and role behind it.

Choose a person

Faces as the options of a radio group: the arrow keys move the choice, and the chosen face takes a tick.

With a hover card

A comment author's face links to the profile and previews it on hover or focus.

In a sentence

A face sized to the line of text, hidden because the name beside it is what gets read.

Profile photo

Larger than lg for a profile header, with a named change-photo control on the inline-end corner.

Workspaces

A workspace is a square mark and a person a round one, so a switcher never mixes them up.

Deactivated

A member who has left stays in the list: the face recedes, a mark and the words say why.

No one yet

Empty seats as dashed avatars above the action that fills them.

Right to left

The same presence and stack in Arabic: the dot crosses to the bottom-left, and the stack overlaps the other way.

Props

PropTypeDefaultDescription
size"default" | "sm" | "lg""default"32px, 24px or 40px.

Dependencies

Source

components/ui/avatar.tsx
"use client"

import * as React from "react"
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
import { cn } from "@/lib/utils"

/**
 * A name on a face needs a role to live on: `aria-label` on a span with none
 * is prohibited, so screen readers drop it (axe aria-prohibited-attr — the
 * files browser's owner column, 25 faces, was nothing but these). A face or a
 * badge that is given a name is an image of that name.
 */
function namedRole(props: { role?: React.AriaRole; "aria-label"?: string; "aria-labelledby"?: string }) {
  return props.role ?? (props["aria-label"] || props["aria-labelledby"] ? "img" : undefined)
}

function Avatar({
  className,
  size = "default",
  ...props
}: AvatarPrimitive.Root.Props & {
  size?: "default" | "sm" | "lg"
}) {
  return (
    <AvatarPrimitive.Root
      data-slot="avatar"
      data-size={size}
      role={namedRole(props)}
      className={cn(
        "group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
        className
      )}
      {...props}
    />
  )
}

function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
  return (
    <AvatarPrimitive.Image
      data-slot="avatar-image"
      className={cn(
        "aspect-square size-full rounded-full object-cover",
        className
      )}
      {...props}
    />
  )
}

function AvatarFallback({
  className,
  ...props
}: AvatarPrimitive.Fallback.Props) {
  return (
    <AvatarPrimitive.Fallback
      data-slot="avatar-fallback"
      className={cn(
        "flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
        className
      )}
      {...props}
    />
  )
}

function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="avatar-badge"
      role={namedRole(props)}
      className={cn(
        // On the inline end, where a line of text ends: the bottom-right
        // corner in English and the bottom-left in a right-to-left page.
        "absolute end-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
        "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
        "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
        "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
        className
      )}
      {...props}
    />
  )
}

function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="avatar-group"
      className={cn(
        "group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
        className
      )}
      {...props}
    />
  )
}

function AvatarGroupCount({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="avatar-group-count"
      className={cn(
        "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
        className
      )}
      {...props}
    />
  )
}

export {
  Avatar,
  AvatarImage,
  AvatarFallback,
  AvatarGroup,
  AvatarGroupCount,
  AvatarBadge,
}