Skip to content
Pebble Pro is here at $49 one timeSee what’s included ↗
Pebble
ComponentsDocsMCPPricing
⌕ Find a component↗Log in
All components 673

Categories

Core inputs & forms46Buttons & actions33Navigation36Overlays & feedback49Data display33Charts & data viz40Cards & layout43Marketing blocks38App shells & pages37AI interfaces40Text & typography effects33Backgrounds & visual effects38Motion & transitions32Numbers & counters32Media34E-commerce333D & interactive36Themed sets40
Start with the essentials.Installation guide ↗Connect your agent ↗
← All componentsData display

Avatar

A round image for a person or entity that falls back to initials while loading or on error.

Preview

IAIAIAIAIATFTFTFTFTF

Reviewers · 4

  • IA

    Ines Alvarez

    Reviewing

    Design lead
  • MO

    Malik Okoye

    In a call until 15:30

    Platform
  • PR

    Priya Raman

    Back in 20 min

    Research
  • TF

    Tomás Ferreira

    Last seen yesterday

    Support
IAMOPRTF+12

16 editing

IAMOPR

Install

Add the editable source with the shadcn CLI. Your project also needs the shared theme tokens this component uses. Setup and requirements. The Pebble installer is not published yet.

npx shadcn@latest add https://pebbleui.com/r/avatar.json

Props

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"Avatar and AvatarGroupCount: 20, 24, 32, 40 or 48 px. Fallback text scales with it.
status"online" | "away" | "busy" | "offline""online"AvatarStatus: presence dot on the avatar's bottom-right edge.
labelstring-AvatarStatus: accessible name; defaults to the status in words ("Online").
AvatarStatusReact.ComponentProps<"span">-Place inside Avatar; role=img with a page-coloured cutout ring.
AvatarGroupReact.ComponentProps<"div">-Overlapping row of Avatars; each lifts on hover.
AvatarGroupCountReact.ComponentProps<"span">-The "+N" chip that ends an AvatarGroup; takes the same size prop.
srcstring-AvatarImage: image URL; shown only after it loads.
altstring""AvatarImage: empty (decorative) by default; pass a name when the avatar stands alone.
onLoadingStatusChange(status: "idle" | "loading" | "loaded" | "error") => void-AvatarImage: reports the image loading status.
delayMsnumber-AvatarFallback: wait before showing the fallback, to avoid a flash on fast loads.
classNamestring-Wins over the defaults on every part.
Build notes
Built on the Radix Avatar primitive: `Avatar.Image` probes the `src` with an
off-DOM `new Image()` and reports `idle → loading → loaded | error`, and
`Avatar.Fallback` renders only while the status is not `loaded`, so there is no
broken-image icon and no layout jump. The root is a `size-*` flex box with
`overflow-hidden rounded-full`; the image fills it with `object-cover` and
`aspect-square`. Size lives in a `sizes` map applied to the root, which also
sets the font size so the fallback's initials inherit it (instead of the
source's group-data selectors). `data-size` is still set on the root for
styling hooks. The demo draws an inline `data:` SVG portrait so no network
image is fetched. Differences from the source: `md` instead of `default` for
the middle size, `alt` defaults to `""`, `object-cover` on the image,
`font-medium` initials, and AvatarBadge / AvatarGroup / AvatarGroupCount are
not shipped.

**Pebble (2026-11-03).** The root no longer clips (`overflow-hidden` dropped);
image and fallback take `rounded-[inherit]` so a status dot can sit on the
edge. A `pointer-events-none` `avatar-ring` span drawn after the children puts
an inset `ring-hairline` over the picture, so pale photos keep an edge. The
fallback is a muted pebble with `inset-shadow-highlight` and `-0.01em`
tracking. The loaded image fades in with `transition-opacity
duration-(--motion-base) ease-enter starting:opacity-0` (no keyframes needed).
New `xs` and `xl` sizes. New parts written from scratch: `AvatarStatus`
(30 % dot, `outline-2 outline-background` cut-out, named for screen readers),
`AvatarGroup` (`-space-x-2`, page-coloured outlines via
`*:data-[slot=avatar]:`, motion-safe hover lift) and `AvatarGroupCount` (a
raised `shadow-raised-xs` chip with tabular numbers). The demo shows every
size with image and initials, a presence list with all four statuses and two
groups.

<!-- PEBBLE-LICENSE -->
View source

registry/data/avatar/avatar.tsx

"use client";

/* PEBBLE-LICENSE
Built by Pebble. Component: avatar.

First-party license: MIT

The following first-party terms apply only to original Pebble material.

MIT License

Copyright (c) 2026 Praxys Solutions LLC (Pebble)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

THIRD-PARTY NOTICES
These notices apply to identified upstream portions; Pebble terms do not replace or narrow their rights.

Upstream: https://raw.githubusercontent.com/shadcn-ui/ui/main/LICENSE.md
MIT License

Copyright (c) 2023 shadcn

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
END PEBBLE-LICENSE */

import * as React from "react";
import { Avatar as AvatarPrimitive } from "radix-ui";

import { cn } from "@/lib/utils";

const sizes = {
  xs: "size-5 text-[0.5625rem]",
  sm: "size-6 text-[0.625rem]",
  md: "size-8 text-xs",
  lg: "size-10 text-sm",
  xl: "size-12 text-base",
} as const;

export type AvatarProps = React.ComponentProps<typeof AvatarPrimitive.Root> & {
  size?: keyof typeof sizes;
};

export function Avatar({ size = "md", className, children, ...props }: AvatarProps) {
  return (
    <AvatarPrimitive.Root
      data-slot="avatar"
      data-size={size}
      className={cn(
        // No overflow clip on the root, so a status dot can sit on the edge.
        // Image and fallback inherit the radius instead.
        "relative inline-flex shrink-0 rounded-full select-none",
        sizes[size],
        className,
      )}
      {...props}
    >
      {children}
      {/* A hairline drawn over the picture keeps light photos from bleeding
          into a light page: the Pebble ring-as-border. */}
      <span
        aria-hidden
        data-slot="avatar-ring"
        className="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-hairline ring-inset"
      />
    </AvatarPrimitive.Root>
  );
}

export function AvatarImage({
  className,
  alt = "",
  ...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
  return (
    <AvatarPrimitive.Image
      data-slot="avatar-image"
      alt={alt}
      className={cn(
        "aspect-square size-full rounded-[inherit] object-cover",
        // Radix mounts the image once it has loaded; it fades in over the
        // fallback instead of popping. Opacity only, so reduced motion keeps it.
        "transition-opacity duration-(--motion-base) ease-enter starting:opacity-0",
        className,
      )}
      {...props}
    />
  );
}

export function AvatarFallback({
  className,
  ...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
  return (
    <AvatarPrimitive.Fallback
      data-slot="avatar-fallback"
      className={cn(
        // Text size comes from the root's size, so initials scale with it. A
        // muted pebble with a top highlight, so initials read as an object.
        "flex size-full items-center justify-center rounded-[inherit] bg-muted font-medium tracking-[-0.01em] text-muted-foreground inset-shadow-highlight",
        className,
      )}
      {...props}
    />
  );
}

// Forced colors drop author backgrounds and box-shadow rings, so each status
// gets its own system colour there: solid `CanvasText`, `GrayText`,
// `Highlight`, and a hollow `CanvasText` border for offline.
const statuses = {
  online: { cls: "bg-success forced-colors:bg-[CanvasText]", label: "Online" },
  away: { cls: "bg-warning forced-colors:bg-[GrayText]", label: "Away" },
  busy: { cls: "bg-danger forced-colors:bg-[Highlight]", label: "Busy" },
  offline: {
    cls: "bg-card ring-[1.5px] ring-border-control ring-inset forced-colors:border-[1.5px] forced-colors:border-[CanvasText] forced-colors:bg-[Canvas]",
    label: "Offline",
  },
} as const;

export type AvatarStatusProps = React.ComponentProps<"span"> & {
  status?: keyof typeof statuses;
  /** Accessible name; defaults to the status in words ("Online"). */
  label?: string;
};

/** A presence dot on the avatar's bottom-right edge, cut out of the page. */
export function AvatarStatus({ status = "online", label, className, ...props }: AvatarStatusProps) {
  const s = statuses[status];
  return (
    <span
      data-slot="avatar-status"
      data-status={status}
      role="img"
      aria-label={label ?? s.label}
      className={cn(
        "absolute right-0 bottom-0 z-10 size-[30%] min-h-1.5 min-w-1.5 rounded-full",
        // The outline matches the page so the dot looks cut out of the avatar.
        "outline-2 outline-background",
        // Forced colors keep the cut-out in the page colour.
        "forced-colors:outline-[Canvas]",
        s.cls,
        className,
      )}
      {...props}
    />
  );
}

/**
 * Overlapping row of avatars. Each one is outlined in the page colour so the
 * overlap reads as a stack, and lifts a step on hover to be picked out.
 */
export function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="avatar-group"
      className={cn(
        "flex items-center -space-x-2",
        "*:data-[slot=avatar]:outline-2 *:data-[slot=avatar]:outline-background",
        "*:data-[slot=avatar]:motion-safe:transition-transform *:data-[slot=avatar]:motion-safe:duration-(--motion-fast) *:data-[slot=avatar]:motion-safe:ease-enter",
        "*:data-[slot=avatar]:hover:z-10 *:data-[slot=avatar]:hover:-translate-y-0.5",
        className,
      )}
      {...props}
    />
  );
}

/** The "+N" chip that ends an AvatarGroup. */
export function AvatarGroupCount({
  size = "md",
  className,
  ...props
}: React.ComponentProps<"span"> & { size?: keyof typeof sizes }) {
  return (
    <span
      data-slot="avatar-group-count"
      className={cn(
        "relative inline-flex shrink-0 items-center justify-center rounded-full bg-card font-medium text-muted-foreground tabular-nums",
        "shadow-raised-xs ring-1 ring-hairline outline-2 outline-background",
        sizes[size],
        className,
      )}
      {...props}
    />
  );
}
Pebble

Small details.
Better interfaces.

React components to explore, make your own, and build on.

Library

  • Components
  • Installation
  • Pricing

Resources

  • For agents
  • About
  • Support

Legal

  • License
  • Terms
  • Privacy
  • Final sale

Built by Pebble

A product of Praxys Solutions LLC ↗

Pebble Pro is live

$49 one time, lifetime updates ↗