stream/ui
Browse the library

Avatar

Initials in a toned circle, plus a group that overlaps them.

betaAvatar.tsx

Examples

Sizes

avatar/sizes.tsx
import { Avatar } from "@streamui/react";

export default function AvatarSizes() {
  return (
    <div className="flex items-center gap-4">
      <Avatar initials="AL" name="Ada Lovelace" size="sm" tone="pink" />
      <Avatar initials="GH" name="Grace Hopper" size="md" tone="lime" />
      <Avatar initials="KJ" name="Katherine Johnson" size="lg" tone="blue" />
    </div>
  );
}

Group

avatar/group.tsx
import { Avatar, AvatarGroup } from "@streamui/react";

const team = [
  { initials: "AL", name: "Ada Lovelace", tone: "lime" },
  { initials: "GH", name: "Grace Hopper", tone: "pink" },
  { initials: "KJ", name: "Katherine Johnson", tone: "blue" },
  { initials: "MH", name: "Margaret Hamilton", tone: "violet" },
] as const;

export default function AvatarGroupExample() {
  return (
    <AvatarGroup aria-label="Project members">
      {team.map((m) => (
        <Avatar key={m.initials} initials={m.initials} name={m.name} tone={m.tone} />
      ))}
      <Avatar initials="+6" name="6 more members" tone="paper" />
    </AvatarGroup>
  );
}

Installation

bun add @streamui/react
usage.tsx
import { Avatar, AvatarGroup } from "@streamui/react";

<Avatar initials="AL" name="Ada Lovelace" />

API

Avatar
PropTypeDefaultDescription
initialsstring—One or two characters.
namestring—Full name, announced instead of the initials.
toneTone | "paper""lime"Fill color.
size"sm" | "md" | "lg""md"Diameter.
AvatarGroup
PropTypeDefaultDescription
...propsComponentProps<"div">—Renders role="group"; give it an aria-label.

Accessibility

  • With a name, the avatar is role="img" and reads the full name instead of the initials.
  • AvatarGroup is a role="group", so an aria-label describes the whole stack.

Source

packages/react/src/Avatar.tsx
import type { ComponentProps } from "react";
import { cn } from "./cn";
import { toneBg, type Tone } from "./tones";

export type AvatarSize = "sm" | "md" | "lg";

const sizes: Record<AvatarSize, string> = {
  sm: "size-9 text-xs",
  md: "size-12 text-sm",
  lg: "size-16 text-base",
};

export type AvatarProps = ComponentProps<"span"> & {
  /** One or two characters, e.g. "AK". */
  initials: string;
  /** Full name, announced to screen readers. */
  name?: string;
  tone?: Tone | "paper";
  size?: AvatarSize;
};

export function Avatar({
  initials,
  name,
  tone = "lime",
  size = "md",
  className,
  ...props
}: AvatarProps) {
  return (
    <span
      role={name ? "img" : undefined}
      aria-label={name}
      className={cn(
        "grid shrink-0 place-items-center rounded-full border-2 border-ink font-mono font-bold",
        tone === "paper" ? "bg-paper-raised text-ink" : cn(toneBg[tone], "text-on-bright"),
        sizes[size],
        className,
      )}
      {...props}
    >
      <span aria-hidden={name ? true : undefined}>{initials}</span>
    </span>
  );
}

/** Overlaps its children like a stack of coins. */
export function AvatarGroup({ className, ...props }: ComponentProps<"div">) {
  return <div role="group" className={cn("flex items-center [&>*+*]:-ml-3", className)} {...props} />;
}