stream/ui
Browse the library

Comic Avatar

Initials on a starburst, seal, splat, or blob sticker, with an ink outline that follows the shape.

betaComicAvatar.tsx

Examples

Shapes, tones, and sizes

comic-avatar/shapes.tsx
import { ComicAvatar } from "@streamuilib/react";

export default function ComicAvatarShapes() {
  return (
    <div className="flex flex-wrap items-center gap-6">
      <ComicAvatar initials="AL" name="Ada Lovelace" shape="blob" size="sm" />
      <ComicAvatar initials="GH" name="Grace Hopper" shape="burst" tone="pink" tilt={-8} />
      <ComicAvatar initials="KJ" name="Katherine Johnson" shape="seal" tone="blue" />
      <ComicAvatar initials="MH" name="Margaret Hamilton" shape="splat" tone="orange" size="lg" tilt={6} />
    </div>
  );
}

Group

Stickers slapped on at alternating tilts. Hover one.

comic-avatar/group.tsx
import { ComicAvatar, ComicAvatarGroup } from "@streamuilib/react";

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

export default function ComicAvatarGroupExample() {
  return (
    <ComicAvatarGroup aria-label="Project members">
      {team.map((m) => (
        <ComicAvatar key={m.initials} initials={m.initials} name={m.name} tone={m.tone} shape={m.shape} />
      ))}
      <ComicAvatar initials="+6" name="6 more members" tone="paper" shape="splat" />
    </ComicAvatarGroup>
  );
}

Installation

bun add @streamuilib/react
Open in v0 (opens in a new tab)
usage.tsx
import { ComicAvatar } from "@streamuilib/react";

<ComicAvatar initials="AL" name="Ada Lovelace" shape="burst" />

API

ComicAvatar
PropTypeDefaultDescription
initialsstring—One or two characters.
namestring—Full name; gives it role="img" and an accessible name.
shape"blob" | "burst" | "seal" | "splat""blob"The sticker shape.
tone"lime" | "pink" | "blue" | "violet" | "orange" | "paper""lime"Fill color.
size"sm" | "md" | "lg""md"Avatar size.
tiltnumber0Tilt in degrees.
ComicAvatarGroup
PropTypeDefaultDescription
...propsComponentProps<"div">—A role="group"; name it with aria-label.

Accessibility

  • With a name it is role="img" with that label, so screen readers hear the full name, not the initials.
  • The group is a labelled role="group".
  • The hover jump stops for reduced motion.

Source

packages/react/src/ComicAvatar.tsx
import type { ComponentProps } from "react";
import { cn } from "./cn";
import { SHAPES } from "./ComicBadge";
import type { AvatarSize } from "./Avatar";
import type { Tone } from "./tones";

export type ComicAvatarShape = "burst" | "seal" | "splat" | "blob";

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

const sizes: Record<AvatarSize, string> = {
  sm: "size-11 text-xs",
  md: "size-15 text-sm",
  lg: "size-20 text-base",
};
const fills: Record<Tone | "paper", string> = {
  lime: "fill-lime",
  pink: "fill-pink",
  blue: "fill-blue",
  violet: "fill-violet",
  orange: "fill-orange",
  paper: "fill-paper-raised",
};

/**
 * A comic-book avatar: initials on a starburst, scalloped seal, splat, or
 * blob, with an ink outline and a hard shadow that follow the shape. It
 * jumps and turns a little on hover. Motion stops for reduced motion.
 */
export function ComicAvatar({
  initials,
  name,
  shape = "blob",
  tone = "lime",
  size = "md",
  tilt = 0,
  className,
  style,
  ...props
}: ComicAvatarProps) {
  return (
    <span
      role={name ? "img" : undefined}
      aria-label={name}
      className={cn(
        "sui-comic-avatar relative grid shrink-0 place-items-center font-mono font-bold",
        tone === "paper" ? "text-ink" : "text-on-bright",
        sizes[size],
        className,
      )}
      style={{ rotate: `${tilt}deg`, ...style }}
      {...props}
    >
      <svg aria-hidden viewBox="0 0 100 100" overflow="visible" className="absolute inset-0 size-full drop-shadow-[3px_3px_0_var(--ink)]">
        <path d={SHAPES[shape].d} className={cn("stroke-ink", fills[tone])} strokeWidth={6} strokeLinejoin="round" />
      </svg>
      <span aria-hidden={name ? true : undefined} className="relative">
        {initials}
      </span>
    </span>
  );
}

/** Overlaps its children like stickers slapped on a page, each at its own tilt. */
export function ComicAvatarGroup({ className, ...props }: ComponentProps<"div">) {
  return (
    <div
      role="group"
      className={cn("flex items-center [&>*+*]:-ml-3 [&>*:nth-child(odd)]:-rotate-6 [&>*:nth-child(even)]:rotate-6", className)}
      {...props}
    />
  );
}