Comic Animated Icon
AnimatedIcon on a comic sticker: impact lines burst out every time the icon animates.
betaComicAnimatedIcon.tsx
Examples
Shapes and tones
Hover each sticker.
comic-animated-icon/shapes.tsx
import { Bell, Heart, Rocket, Zap } from "lucide-react";
import { ComicAnimatedIcon } from "@streamuilib/react";
export default function ComicAnimatedIconShapes() {
return (
<div className="flex flex-wrap items-center justify-center gap-8 py-4">
<ComicAnimatedIcon animation="shake" shape="burst" tone="lime">
<Bell aria-hidden strokeWidth={2.5} />
</ComicAnimatedIcon>
<ComicAnimatedIcon animation="pop" shape="seal" tone="pink">
<Heart aria-hidden strokeWidth={2.5} />
</ComicAnimatedIcon>
<ComicAnimatedIcon animation="bounce" shape="blob" tone="blue" size="lg">
<Rocket aria-hidden strokeWidth={2.5} />
</ComicAnimatedIcon>
<ComicAnimatedIcon animation="wiggle" shape="splat" tone="orange" size="sm">
<Zap aria-hidden strokeWidth={2.5} />
</ComicAnimatedIcon>
</div>
);
}Triggers
hover
loop
once
comic-animated-icon/triggers.tsx
import { RefreshCw, Sparkles, Star } from "lucide-react";
import { ComicAnimatedIcon } from "@streamuilib/react";
export default function ComicAnimatedIconTriggers() {
return (
<div className="flex flex-wrap items-start justify-center gap-10 py-4 text-center">
<div className="flex flex-col items-center gap-3">
<ComicAnimatedIcon animation="spin" tone="lime">
<RefreshCw aria-hidden strokeWidth={2.5} />
</ComicAnimatedIcon>
<span className="font-mono text-xs">hover</span>
</div>
<div className="flex flex-col items-center gap-3">
<ComicAnimatedIcon animation="pulse" trigger="loop" shape="seal" tone="violet">
<Sparkles aria-hidden strokeWidth={2.5} />
</ComicAnimatedIcon>
<span className="font-mono text-xs">loop</span>
</div>
<div className="flex flex-col items-center gap-3">
<ComicAnimatedIcon animation="draw" trigger="once" shape="blob" tone="pink" duration={1200}>
<Star aria-hidden strokeWidth={2.5} />
</ComicAnimatedIcon>
<span className="font-mono text-xs">once</span>
</div>
</div>
);
}Installation
bun add @streamuilib/reactusage.tsx
import { Bell } from "lucide-react";
import { ComicAnimatedIcon } from "@streamuilib/react";
<ComicAnimatedIcon animation="shake" shape="burst">
<Bell aria-hidden />
</ComicAnimatedIcon>API
| Prop | Type | Default | Description |
|---|---|---|---|
| animation | "draw" | "spin" | "bounce" | "wiggle" | "pulse" | "shake" | "pop" | — | The icon's motion. Required. |
| trigger | "hover" | "loop" | "once" | "hover" | When it plays; the impact lines burst at the same time. |
| shape | "burst" | "seal" | "splat" | "blob" | "burst" | The sticker behind the icon. |
| tone | "lime" | "pink" | "blue" | "violet" | "orange" | "paper" | "lime" | Sticker color. |
| size | "sm" | "md" | "lg" | "md" | Sticker size; the icon scales with it. |
| duration | number | — | Length of one cycle in ms. |
Accessibility
- Decorative by default: give the icon aria-hidden and label whatever it sits in.
- Every animation, the sticker turn, and the impact lines stop for reduced motion.
Source
packages/react/src/ComicAnimatedIcon.tsx
"use client";
import { cn } from "./cn";
import { AnimatedIcon, type AnimatedIconProps } from "./AnimatedIcon";
import { SHAPES } from "./ComicBadge";
import type { Tone } from "./tones";
export type ComicAnimatedIconProps = AnimatedIconProps & {
/** The sticker behind the icon. */
shape?: "burst" | "seal" | "splat" | "blob";
tone?: Tone | "paper";
size?: "sm" | "md" | "lg";
};
const sizes = { sm: "size-12 [&_svg.lucide]:size-5", md: "size-16 [&_svg.lucide]:size-7", lg: "size-22 [&_svg.lucide]:size-10" };
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",
};
/**
* AnimatedIcon on a comic sticker: the icon sits on a starburst, seal, splat,
* or blob with an ink outline, and impact lines burst out around it whenever
* the animation plays (on hover, on a loop, or once). Decorative unless you
* label it; everything stops for reduced motion.
*/
export function ComicAnimatedIcon({
shape = "burst",
tone = "lime",
size = "md",
trigger = "hover",
className,
...props
}: ComicAnimatedIconProps) {
return (
<span
data-trigger={trigger}
className={cn("sui-comic-icon relative inline-grid shrink-0 place-items-center text-ink", sizes[size], className)}
>
<svg aria-hidden viewBox="-60 -60 120 120" overflow="visible" className="sui-comic-icon-lines pointer-events-none absolute -inset-1/4 size-[150%]">
<g stroke="var(--ink)" strokeWidth="5" strokeLinecap="round">
{[0, 45, 90, 135, 180, 225, 270, 315].map((a) => (
<line key={a} x1="0" y1="-44" x2="0" y2="-56" transform={`rotate(${a})`} />
))}
</g>
</svg>
<svg aria-hidden viewBox="0 0 100 100" overflow="visible" className="sui-comic-icon-shape absolute inset-0 size-full drop-shadow-[3px_3px_0_var(--ink)]">
<path d={SHAPES[shape].d} className={cn("stroke-ink", fills[tone])} strokeWidth={5} strokeLinejoin="round" />
</svg>
<AnimatedIcon trigger={trigger} className="relative" {...props} />
</span>
);
}