stream/ui
Browse the library

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/react
Open in v0 (opens in a new tab)
usage.tsx
import { Bell } from "lucide-react";
import { ComicAnimatedIcon } from "@streamuilib/react";

<ComicAnimatedIcon animation="shake" shape="burst">
  <Bell aria-hidden />
</ComicAnimatedIcon>

API

PropTypeDefaultDescription
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.
durationnumber—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>
  );
}