{"slug":"drawing-icon","category":"loaders","job":"oversee","added":"2026-11-05T11:03Z","title":"Drawing Icon","description":"A line icon that draws itself stroke by stroke while the step it stands for is running.","url":"https://ai-patterns.app/patterns/loaders/drawing-icon","markdown":"https://ai-patterns.app/patterns/loaders/drawing-icon.md","json":"https://ai-patterns.app/api/patterns/loaders/drawing-icon","seenIn":[{"team":"chatgpt","source":"https://chatgpt.com"}],"spec":"# Drawing Icon\n\n## Summary\nA line icon that draws itself stroke by stroke, holds for a beat, then erases and starts again — used as the \"working\" glyph for a specific kind of activity (searching, thinking, writing, making an image). Because it's the activity's own icon rather than a generic spinner, it says *what* is happening as well as *that* something is happening.\n\n## When to use\n- Next to a status label while an agent runs a step whose kind you know (\"Searching the web\", \"Creating image\").\n- In tool-call chips, step lists or traces where each step type already has an icon — animate the icon of the step that is running.\n- When duration is unknown and you want a calmer, more characterful signal than a spinner.\n\n## When not to use\n- When you know real progress — use a progress bar.\n- For more than one running step in the same view; several icons drawing at once reads as noise. Animate only the active one and show the rest static.\n- With dense, filled or very detailed icons — the effect needs a few clean strokes to read.\n- As decoration on idle UI. Motion should mean \"working right now\".\n\n## Anatomy\n- Icon: a 24×24 stroke icon split into its individual paths (2–7 strokes works best), drawn in path order.\n- Optional ghost: a faint, static copy of the full icon underneath, so the shape is recognisable at every frame, including the empty one.\n- Label (outside the component): the present-participle verb for the step.\n\n## Behavior\n- Each stroke animates its dash offset: draw on (~45% of the loop), hold fully drawn (~20%), then slide off the far end (~35%), easing in and out. Default loop is 3.6s.\n- Strokes start staggered (default 0.3s apart), so the icon builds up in sequence rather than all at once.\n- Works for any path: every stroke is normalised with `pathLength=\"1\"`, so one keyframe set fits every shape.\n- When the step changes, swap the icon (remount it so it starts drawing from empty) and the label together.\n- When the step finishes, pause it: it settles on the fully drawn icon, which then becomes the step's static \"done\" glyph.\n\n## Content guidelines\n- Pick the icon that matches the actual activity; don't show a globe unless something is actually being searched.\n- Pair it with a short label — the icon alone is a hint, not a status.\n- Order paths so the drawing reads naturally: outline first, details last.\n\n## Accessibility\n- The icon is decorative (`aria-hidden`) when a visible label sits next to it; otherwise pass a `label` so it's announced as an image.\n- Put the status label in an `aria-live=\"polite\"` region so step changes are announced.\n- With `prefers-reduced-motion`, the icon is shown fully drawn and only gently pulses its opacity.\n\n## Related patterns\n- **Thinking Loader** — Use it as well, replacing its static glyph with a Drawing Icon for the step that is running.\n- **Tool Call Chip** — Use it as well to animate the chip's icon while the call runs.\n- **Analysis List** — Use it as well to animate only the active step's icon.\n","code":"\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Stroke paths for a few 24×24 line icons (Lucide, ISC). Circles are written\n * as two arcs so every shape is a `<path>` that can be \"drawn\".\n */\nexport const DRAWING_ICONS = {\n  origami: [\n    \"M12 12V4a1 1 0 0 1 1-1h6.297a1 1 0 0 1 .651 1.759l-4.696 4.025\",\n    \"m12 21-7.414-7.414A2 2 0 0 1 4 12.172V6.415a1.002 1.002 0 0 1 1.707-.707L20 20.009\",\n    \"m12.214 3.381 8.414 14.966a1 1 0 0 1-.167 1.199l-1.168 1.163a1 1 0 0 1-.706.291H6.351a1 1 0 0 1-.625-.219L3.25 18.8a1 1 0 0 1 .631-1.781l4.165.027\",\n  ],\n  globe: [\n    \"M2 12a10 10 0 1 0 20 0a10 10 0 1 0-20 0\",\n    \"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20\",\n    \"M2 12h20\",\n  ],\n  lightbulb: [\n    \"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\",\n    \"M9 18h6\",\n    \"M10 22h4\",\n  ],\n  pen: [\n    \"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\",\n    \"M13 21h8\",\n  ],\n  telescope: [\n    \"m10.065 12.493-6.18 1.318a.934.934 0 0 1-1.108-.702l-.537-2.15a1.07 1.07 0 0 1 .691-1.265l13.504-4.44\",\n    \"M16.485 5.94a2 2 0 0 1 1.455-2.425l1.09-.272a1 1 0 0 1 1.212.727l1.515 6.06a1 1 0 0 1-.727 1.213l-1.09.272a2 2 0 0 1-2.425-1.455z\",\n    \"m13.56 11.747 4.332-.924\",\n    \"m6.158 8.633 1.114 4.456\",\n    \"M10 13a2 2 0 1 0 4 0a2 2 0 1 0-4 0\",\n    \"m8 21 3.105-6.21\",\n    \"m16 21-3.105-6.21\",\n  ],\n} satisfies Record<string, string[]>;\n\nexport type DrawingIconName = keyof typeof DRAWING_ICONS;\n\nexport interface DrawingIconProps {\n  /** A preset name, or your own list of 24×24 stroke path `d` strings, drawn in order. */\n  icon: DrawingIconName | string[];\n  /** Rendered size in px. */\n  size?: number;\n  /** Length of one draw → hold → erase loop, in seconds. */\n  duration?: number;\n  /** Delay between consecutive strokes starting, in seconds. */\n  stagger?: number;\n  /** Keep a faint copy of the full icon underneath so its shape is always readable. */\n  ghost?: boolean;\n  /** Pause on the fully drawn icon (e.g. when the work it stands for finishes). */\n  paused?: boolean;\n  /** Accessible label. Omit when a visible text label sits next to the icon. */\n  label?: string;\n  className?: string;\n}\n\n/*\n * Every path gets pathLength=\"1\", so a dasharray of 1 is exactly one stroke\n * long whatever the real geometry: offset 1 hides it, 0 shows it, -1 slides\n * it off the far end. One keyframe set then draws any shape.\n */\nconst STYLES = `\n@keyframes drawing-icon-stroke {\n  0% { stroke-dashoffset: 1; }\n  45%, 65% { stroke-dashoffset: 0; }\n  100% { stroke-dashoffset: -1; }\n}\n.drawing-icon [data-stroke] {\n  stroke-dasharray: 1;\n  stroke-dashoffset: 1;\n  animation: drawing-icon-stroke var(--draw-duration) ease-in-out infinite;\n  animation-delay: calc(var(--i) * var(--draw-stagger));\n}\n.drawing-icon[data-paused] [data-stroke] {\n  animation: none;\n  stroke-dashoffset: 0;\n  transition: stroke-dashoffset 0.3s ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .drawing-icon [data-stroke] {\n    animation: none;\n    stroke-dashoffset: 0;\n  }\n  .drawing-icon:not([data-paused]) {\n    animation: drawing-icon-pulse 1.6s ease-in-out infinite;\n  }\n}\n@keyframes drawing-icon-pulse {\n  50% { opacity: 0.45; }\n}\n`;\n\nexport function DrawingIcon({\n  icon,\n  size = 16,\n  duration = 3.6,\n  stagger = 0.3,\n  ghost = false,\n  paused = false,\n  label,\n  className,\n}: DrawingIconProps) {\n  const paths = typeof icon === \"string\" ? DRAWING_ICONS[icon] : icon;\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      role={label ? \"img\" : undefined}\n      aria-label={label}\n      aria-hidden={label ? undefined : true}\n      data-paused={paused ? \"\" : undefined}\n      className={cn(\"drawing-icon shrink-0\", className)}\n      style={\n        {\n          \"--draw-duration\": `${duration}s`,\n          \"--draw-stagger\": `${stagger}s`,\n        } as React.CSSProperties\n      }\n    >\n      <style>{STYLES}</style>\n      {ghost && (\n        <g opacity={0.15}>\n          {paths.map((d, i) => (\n            <path key={i} d={d} />\n          ))}\n        </g>\n      )}\n      {paths.map((d, i) => (\n        <path\n          key={i}\n          d={d}\n          pathLength={1}\n          data-stroke=\"\"\n          style={{ \"--i\": i } as React.CSSProperties}\n        />\n      ))}\n    </svg>\n  );\n}\n","dependencies":[],"localImports":["@/lib/utils"]}