# Avatar Status Ring — React Avatar Component

> An avatar wrapped in a presence ring: online radiates soft pulses, away dims to amber, busy holds a solid red, and the badge pops when status changes.

- Category: [Avatars](https://pixelpapa.com/components/avatars.md)
- Tags: avatar, status, presence, ring, pulse
- Dependencies: motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/avatars/avatar-status-ring

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/avatar-status-ring.json"
```

The file is written to `components/pixelpapa/avatar-status-ring.tsx`. To install manually, run `npm install motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { motion } from "motion/react";
import { useState } from "react";
import { cn } from "@/lib/utils";

export type Presence = "online" | "away" | "busy" | "offline";

type StatusAvatarProps = {
  initials: string;
  name: string;
  status: Presence;
  hue?: number;
  size?: number;
  className?: string;
};

const tone: Record<Presence, { color: string; label: string }> = {
  online: { color: "oklch(0.72 0.19 150)", label: "Online" },
  away: { color: "oklch(0.8 0.16 80)", label: "Away" },
  busy: { color: "oklch(0.64 0.22 25)", label: "Do not disturb" },
  offline: { color: "var(--muted-foreground)", label: "Offline" },
};

export function StatusAvatar({ initials, name, status, hue = 262, size = 72, className }: StatusAvatarProps) {
  const { color, label } = tone[status];
  const badge = Math.round(size * 0.3);
  return (
    <span role="img" aria-label={`${name}, ${label}`} className={cn("relative inline-flex", className)} style={{ width: size, height: size }}>
      <style>{`@keyframes pp-avatar-status-ring-pulse { 0% { transform: scale(1); opacity: .55; } 100% { transform: scale(1.55); opacity: 0; } }`}</style>
      {status === "online" &&
        [0, 1].map((k) => (
          <span
            key={k}
            aria-hidden
            className="absolute inset-0 rounded-full motion-reduce:hidden"
            style={{ border: `2px solid ${color}`, animation: `pp-avatar-status-ring-pulse 2.4s ease-out ${k * 1.2}s infinite` }}
          />
        ))}
      <span
        className="absolute inset-0 rounded-full transition-[box-shadow,opacity] duration-500"
        style={{ boxShadow: `0 0 0 3px var(--background), 0 0 0 5.5px ${color}`, opacity: status === "offline" ? 0.35 : 1 }}
      />
      <span
        className={cn("relative flex size-full items-center justify-center rounded-full font-semibold text-white transition-[filter] duration-500", status === "offline" && "grayscale-[0.8]")}
        style={{ fontSize: size * 0.34, background: `linear-gradient(135deg, oklch(0.78 0.14 ${hue}), oklch(0.54 0.2 ${hue + 40}))` }}
      >
        {initials}
      </span>
      <motion.span
        key={status}
        aria-hidden
        initial={{ scale: 0 }}
        animate={{ scale: 1 }}
        transition={{ type: "spring", stiffness: 500, damping: 18 }}
        className="absolute -right-0.5 -bottom-0.5 flex items-center justify-center rounded-full ring-[3px] ring-background"
        style={{ width: badge, height: badge, background: status === "offline" ? "var(--background)" : color }}
      >
        {status === "busy" && <span className="h-[2.5px] w-1/2 rounded-full bg-white" />}
        {status === "away" && <span className="absolute -top-px -left-px size-[62%] rounded-full bg-background" />}
        {status === "offline" && <span className="size-full rounded-full border-[3px] border-muted-foreground/60" />}
      </motion.span>
    </span>
  );
}

const order: Presence[] = ["online", "away", "busy", "offline"];

const team = [
  { name: "Ayla Demir", initials: "AD", hue: 262, role: "Design" },
  { name: "Bruno Sato", initials: "BS", hue: 30, role: "Platform" },
  { name: "Cleo Marsh", initials: "CM", hue: 320, role: "Support" },
  { name: "Dario Ruiz", initials: "DR", hue: 160, role: "Growth" },
];

export default function AvatarStatusRingDemo() {
  const [statuses, setStatuses] = useState<Presence[]>(order);
  return (
    <div className="flex flex-col items-center gap-8">
      <div className="flex flex-wrap justify-center gap-4">
        {team.map((p, i) => (
          <button
            key={p.name}
            type="button"
            onClick={() => setStatuses((s) => s.map((v, k) => (k === i ? order[(order.indexOf(v) + 1) % order.length] : v)))}
            className="flex w-36 flex-col items-center rounded-3xl border border-border bg-card px-4 py-6 outline-none transition hover:-translate-y-1 hover:shadow-lg hover:shadow-primary/10 focus-visible:ring-2 focus-visible:ring-ring"
          >
            <StatusAvatar name={p.name} initials={p.initials} hue={p.hue} status={statuses[i]} />
            <span className="mt-5 text-sm font-medium text-card-foreground">{p.name}</span>
            <span className="mt-0.5 text-xs text-muted-foreground">{tone[statuses[i]].label}</span>
          </button>
        ))}
      </div>
      <p className="text-xs text-muted-foreground">Click a teammate to cycle their status</p>
    </div>
  );
}
```

## AI prompt

```text
Add the "Avatar Status Ring" component from PixelPapa UI to this project.

What it is: An avatar wrapped in a presence ring: online radiates soft pulses, away dims to amber, busy holds a solid red, and the badge pops when status changes.
Category: Avatars. Depends on: motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/avatar-status-ring.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/avatar-status-ring.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/avatar-status-ring" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More avatars

- [Avatar Group Tooltip Names](https://pixelpapa.com/components/avatars/avatar-group-tooltip-names.md): An avatar row sharing one tooltip that glides from face to face on a spring while the hovered avatar lifts and its neighbours step aside.
- [Avatar Hover Card](https://pixelpapa.com/components/avatars/avatar-hover-card.md) (Pro): An avatar that opens a profile popover on hover or focus, scaling out of the trigger on a spring with forgiving open and close delays.
- [Avatar Stack Expand](https://pixelpapa.com/components/avatars/avatar-stack-expand.md): An overlapping avatar stack that springs apart on hover to show first names, with a +N chip that deals out the rest.
- [Avatar Story Ring](https://pixelpapa.com/components/avatars/avatar-story-ring.md) (Pro): Story avatars with a gradient ring that breaks into spinning dashes while the story loads, then settles into a quiet seen state.
- [Avatar Upload Dropzone](https://pixelpapa.com/components/avatars/avatar-upload-dropzone.md) (Pro): A circular avatar dropzone: its dashed ring spins and swells when a file hovers over it, then the photo drops in over the initials fallback.
