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/reactusage.tsx
import { ComicAvatar } from "@streamuilib/react";
<ComicAvatar initials="AL" name="Ada Lovelace" shape="burst" />API
| Prop | Type | Default | Description |
|---|---|---|---|
| initials | string | — | One or two characters. |
| name | string | — | 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. |
| tilt | number | 0 | Tilt in degrees. |
| Prop | Type | Default | Description |
|---|---|---|---|
| ...props | ComponentProps<"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}
/>
);
}