All patterns

AI Thinking Pulse

A calm breathing dot with a soft expanding ring — the app is thinking, not stuck.

aicalmminimalautomatic · looping · starter · ~1.8s
Variant

The animated component in this preview is rendered from the canonical file shown here. The surrounding demo shell only provides context and is not part of the copied code.

119 lines · react + motion only
import { motion, useReducedMotion } from "motion/react";

/**
 * Vibary · AI Thinking Pulse
 *
 * A calm breathing dot with a soft expanding ring, for open-ended
 * AI waiting states (analyzing, generating, streaming).
 *
 * Self-contained: depends only on `motion` (react ships with your app).
 * Works with zero props; tune via `variant`, `size`, `color`.
 * Requires the automatic JSX runtime (default since React 17).
 */

export type AiThinkingPulseProps = {
  /** Visual character of the motion. */
  variant?: "subtle" | "default" | "playful";
  /** Core dot diameter in px. */
  size?: number;
  /** Dot and ring color. */
  color?: string;
  /** Accessible status label announced to screen readers. */
  label?: string;
};

type VariantConfig = {
  breathScale: number;
  cycleSeconds: number;
  ringCount: number;
};

const VARIANTS: Record<"subtle" | "default" | "playful", VariantConfig> = {
  // Breath only, no ring. For dense UI like list rows.
  subtle: { breathScale: 1.04, cycleSeconds: 2.1, ringCount: 0 },
  // One soft ring per cycle. The all-purpose setting.
  default: { breathScale: 1.15, cycleSeconds: 1.8, ringCount: 1 },
  // Two staggered rings, quicker cycle. For hero moments.
  playful: { breathScale: 1.2, cycleSeconds: 1.3, ringCount: 3 },
};

export default function AiThinkingPulse({
  variant = "default",
  size = 12,
  color = "#7C7CF0",
  label = "AI is thinking",
}: AiThinkingPulseProps) {
  const reduceMotion = useReducedMotion();
  const cfg = VARIANTS[variant];

  // Reduced motion: a static dot. The status stays visible and the
  // label keeps announcing it — liveness is not conveyed by motion alone.
  if (reduceMotion) {
    return (
      <span
        role="status"
        aria-label={label}
        style={{
          display: "inline-block",
          width: size,
          height: size,
          borderRadius: "50%",
          background: color,
          opacity: 0.9,
        }}
      />
    );
  }

  return (
    <span
      role="status"
      aria-label={label}
      style={{
        position: "relative",
        display: "inline-block",
        width: size,
        height: size,
      }}
    >
      {Array.from({ length: cfg.ringCount }, (_, ringIndex) => (
        <motion.span
          key={ringIndex}
          aria-hidden
          initial={{ scale: 1, opacity: 0 }}
          animate={{ scale: [1, 2.6], opacity: [0.35, 0] }}
          transition={{
            duration: cfg.cycleSeconds,
            repeat: Infinity,
            delay: (ringIndex * cfg.cycleSeconds) / cfg.ringCount / 2,
            ease: "easeOut",
          }}
          style={{
            position: "absolute",
            inset: 0,
            borderRadius: "50%",
            border: `1.5px solid ${color}`,
          }}
        />
      ))}
      <motion.span
        aria-hidden
        animate={{
          scale: [1, cfg.breathScale, 1],
          opacity: [0.8, 1, 0.8],
        }}
        transition={{
          duration: cfg.cycleSeconds,
          repeat: Infinity,
          ease: "easeInOut",
        }}
        style={{
          position: "absolute",
          inset: 0,
          borderRadius: "50%",
          background: color,
        }}
      />
    </span>
  );
}

About this pattern

Open-ended waiting states for AI inference: analyzing an image, generating a reply, streaming a response. A spinner promises determinate progress it can't deliver; a breathing pulse communicates "alive and working" without promising when it will finish. The core dot breathes on a slow cycle while a ring expands and fades — quiet enough to sit inside a chat bubble without demanding attention.

AI response pendingImage analysisGenerating contentStreaming pause

Where it shows up

Screens we drew to show where this motion usually sits. Illustrations, not captures of any product.

  • Summarise the supplier contract and flag anything unusual.
    Working through the document…
    Ask a follow-up
    AI assistant

    Breathing dot shown while a response is being generated.

Related patterns