AI Patterns

buttons

Stop Generation Button

A send button that morphs into a stop control mid-stream and back on completion.

"use client";

import * as React from "react";
import { motion } from "motion/react";
import { ArrowUp, Square } from "lucide-react";

import { cn } from "@/lib/utils";

export type GenerationState = "idle" | "generating";

export interface StopGenerationButtonProps {
  state: GenerationState;
  /** Disables the idle/send state (e.g. empty input). Ignored while generating — stopping is never blocked. */
  disabled?: boolean;
  onSubmit?: () => void;
  onStop?: () => void;
  className?: string;
}

export function StopGenerationButton({
  state,
  disabled = false,
  onSubmit,
  onStop,
  className,
}: StopGenerationButtonProps) {
  const generating = state === "generating";

  return (
    <motion.button
      type="button"
      layout
      disabled={!generating && disabled}
      onClick={generating ? onStop : onSubmit}
      aria-label={generating ? "Stop generating" : "Send message"}
      whileTap={{ scale: 0.92 }}
      transition={{ layout: { duration: 0.18, ease: "easeOut" } }}
      className={cn(
        "flex size-8 shrink-0 items-center justify-center rounded-full bg-foreground text-background transition-colors disabled:bg-muted disabled:text-muted-foreground",
        className
      )}
    >
      <motion.span
        key={generating ? "stop" : "send"}
        initial={{ opacity: 0, scale: 0.6 }}
        animate={{ opacity: 1, scale: 1 }}
        transition={{ duration: 0.15 }}
        className="flex items-center justify-center"
      >
        {generating ? <Square className="size-3 fill-current" /> : <ArrowUp className="size-4" />}
      </motion.span>
    </motion.button>
  );
}

A UX spec for this pattern — written for agents implementing or reusing it, not the code.

# Stop Generation Button

## Summary
A composer's send control that morphs into a Stop button the instant generation begins, then morphs back the instant it ends — the same control across all three states (idle, generating, done) so the user's hand never has to find a different button to interrupt a response.

## When to use
- Any composer whose output streams over a real window of time (an LLM response, a long-running job) where the user may want to interrupt it mid-flight.
- As the one control that owns both "start" and "stop" for a single turn — never a second, separately-positioned cancel button.

## When not to use
- For actions that can't actually be interrupted — never show a Stop control that doesn't stop anything real; that's worse than no control at all.
- For near-instant responses with no meaningful in-flight window — the morph would flash and add noise instead of giving the user a real chance to act.

## Anatomy
- A single circular (or pill) icon button.
- Idle state: a send/arrow icon, disabled when the input is empty.
- Generating state: a solid stop-square icon, always enabled.
- The shape morphs between states with motion — not an abrupt icon swap — so it reads as the same control changing purpose, not a different button appearing.

## Behavior
- Clicking while idle (with input present) submits immediately and the button morphs to Stop before the first token of the response arrives — the transition is optimistic, not waiting on a server round-trip.
- Clicking while generating cancels the stream and the button morphs back to idle immediately, without an intermediate loading state on the click itself.
- Disabled only in the idle state with empty input; always interactive while generating, since stopping should never be blocked.
- The morph plays every transition, including generating → idle on natural completion, so a finished response and a user-cancelled one both land back on the same recognizable send affordance.

## Content guidelines
- Icon-only is standard; if a label is shown alongside, it swaps in lockstep with the icon ("Send" / "Stop") rather than staying static.

## Accessibility
- `aria-label` updates with state ("Send message" / "Stop generating") so assistive tech announces the button's current purpose, not just its icon.
- Remains a real, focusable, keyboard-operable button (Enter/Space) in every state.
- Disabled state uses the `disabled` attribute, not opacity alone, so it's programmatically detectable.

## Related patterns
- Thinking Loader is the passive "still working" indicator that typically appears alongside this button — this control is what lets the user act on that same in-progress window.