Agent Setup Pill
A floating pill that copies a setup prompt for the user's AI coding agent in one click — outcome label, a row of supported agents, a tooltip that says what the click does, and a separate dismiss button.
Hover for the tooltip, click to copy the prompt
"use client";
import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { Check, X } from "lucide-react";
import { cn } from "@/lib/utils";
export interface SetupAgent {
id: string;
label: string;
/** A small mark (~16px). Brand marks read best as monochrome or their own colours. */
icon: React.ReactNode;
}
export interface AgentSetupPillProps {
/** Outcome-first label, e.g. "Onboard your agent to Acme". */
label: string;
/** Says what the click does before it happens. */
tooltip?: string;
/** The setup prompt copied to the clipboard. */
prompt: string;
/** The coding agents the prompt works in, shown as a row of marks. */
agents: SetupAgent[];
/** Shown in place of the label for a moment after copying. */
copiedLabel?: string;
onCopy?: () => void;
/** Omit to hide the dismiss button. */
onDismiss?: () => void;
className?: string;
}
const spring = { type: "spring", stiffness: 420, damping: 32 } as const;
export function AgentSetupPill({
label,
tooltip = "Copies a setup prompt for your AI coding tool",
prompt,
agents,
copiedLabel = "Prompt copied — paste it into your agent",
onCopy,
onDismiss,
className,
}: AgentSetupPillProps) {
const reduce = useReducedMotion();
const tooltipId = React.useId();
const [copied, setCopied] = React.useState(false);
const [hint, setHint] = React.useState(false);
const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined);
React.useEffect(() => () => clearTimeout(timer.current), []);
async function copy() {
try {
await navigator.clipboard.writeText(prompt);
} catch {
// Clipboard can be blocked (insecure context, iframe); still confirm.
}
setCopied(true);
setHint(false);
onCopy?.();
clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 2200);
}
return (
<motion.div
initial={reduce ? { opacity: 0 } : { opacity: 0, y: 12, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: 8, scale: 0.96 }}
transition={spring}
className={cn("flex items-center gap-2", className)}
>
<div className="relative">
<AnimatePresence>
{hint && !copied && (
<motion.div
id={tooltipId}
role="tooltip"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.15 }}
className="pointer-events-none absolute bottom-full left-1/2 mb-2 -translate-x-1/2 rounded-lg border bg-popover px-3 py-1.5 text-xs whitespace-nowrap text-popover-foreground shadow-sm"
>
{tooltip}
</motion.div>
)}
</AnimatePresence>
<motion.button
type="button"
onClick={copy}
onPointerEnter={() => setHint(true)}
onPointerLeave={() => setHint(false)}
onFocus={() => setHint(true)}
onBlur={() => setHint(false)}
onKeyDown={(e) => e.key === "Escape" && setHint(false)}
aria-describedby={hint ? tooltipId : undefined}
whileTap={reduce ? undefined : { scale: 0.97 }}
layout={!reduce}
transition={spring}
className="group flex h-10 items-center gap-3 rounded-full border bg-card pr-3 pl-4 text-sm font-medium text-card-foreground shadow-sm transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
>
<span className="relative grid overflow-hidden">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={copied ? "copied" : "idle"}
initial={reduce ? { opacity: 0 } : { opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -12 }}
transition={{ duration: 0.2 }}
className="flex items-center gap-1.5 whitespace-nowrap"
>
{copied && <Check className="size-4 text-emerald-600 dark:text-emerald-400" aria-hidden />}
{copied ? copiedLabel : label}
</motion.span>
</AnimatePresence>
</span>
<AnimatePresence initial={false}>
{!copied && (
<motion.span
aria-hidden
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: "auto" }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.2 }}
className="flex items-center"
>
{agents.map((agent, i) => (
<motion.span
key={agent.id}
title={agent.label}
initial={reduce ? false : { opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ ...spring, delay: reduce ? 0 : 0.15 + i * 0.05 }}
className="grid size-5 place-items-center transition-[margin,transform] duration-200 not-first:ml-0.5 group-hover:not-first:ml-1.5 group-hover:-translate-y-px [&_svg]:size-4"
>
{agent.icon}
</motion.span>
))}
</motion.span>
)}
</AnimatePresence>
</motion.button>
<span className="sr-only" aria-live="polite">
{copied ? copiedLabel : ""}
</span>
</div>
{onDismiss && (
<motion.button
type="button"
onClick={onDismiss}
aria-label="Dismiss"
whileTap={reduce ? undefined : { scale: 0.92 }}
className="grid size-10 shrink-0 place-items-center rounded-full border bg-card text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
>
<X className="size-4" />
</motion.button>
)}
</motion.div>
);
}
A UX spec for this pattern — written for agents implementing or reusing it, not the code.
# Agent Setup Pill
## Summary
A small floating pill that hands setup off to the user's AI coding agent in one click. The label names the outcome ("Onboard your agent to Acme"), a row of agent marks shows which tools it works with, a tooltip says what the click will do, and clicking copies a ready-made setup prompt to the clipboard. A separate round button dismisses it.
## When to use
- Docs sites, dashboards and quickstarts where many users will set the product up through Claude Code, Cursor, Copilot or similar instead of by hand.
- As a lightweight, always-reachable entry point that sits alongside the regular docs — not instead of them.
- When the setup can be described as one self-contained prompt (install, configure, verify).
## When not to use
- Inside a guided onboarding step that needs to wait for a first event — use the Prompt Onboarding card, which shows the prompt and tracks status.
- When the setup needs account-specific secrets the page can't safely put in a prompt on a public page.
- As a general "Ask AI" entry point — this pill only copies a setup prompt; it doesn't open a chat.
## Anatomy
- Pill button: outcome label + a row of 3–5 agent marks.
- Tooltip above the pill: one line describing the action ("Copies a setup prompt for your AI coding tool").
- Copied state: the label swaps to a check and a short instruction; the marks collapse.
- Dismiss button: a separate circular button with an ×, visually detached from the pill so it can't be hit by accident.
## Behavior
- Enters with a short rise-and-fade once the page settles; agent marks stagger in after the label.
- Hover or keyboard focus shows the tooltip; Escape, blur and pointer leave hide it. The tooltip never appears in the copied state.
- On hover, the agent marks fan out slightly as an affordance that the pill is clickable.
- Click copies the prompt. The label rolls to "Prompt copied — paste it into your agent" with a check for ~2s, then returns. The pill resizes smoothly between the two widths.
- If the clipboard API is blocked, still confirm, and make sure the prompt is reachable elsewhere (e.g. a docs page) so the user isn't stuck.
- Dismiss hides the pill with a short exit animation and should persist (per user or per browser) — don't bring it back on the next page view.
## Content guidelines
- Label: outcome-first and product-specific — "Onboard your agent to Acme", "Set up Acme with your agent". Not "Copy prompt".
- Tooltip: one sentence, says exactly what happens — the click copies, it doesn't open anything.
- Prompt: imperative, numbered steps, an explicit definition of done, keep secrets in env vars, and ask the agent to summarize its changes.
- Agent marks: only tools the prompt has actually been tested with; keep the brand marks small and monochrome except where the brand colour is the mark.
## Accessibility
- The pill is a real `<button>`; its accessible name is the visible label. The agent marks are `aria-hidden` decoration (each still has a `title` for sighted pointer users).
- The tooltip uses `role="tooltip"` and is linked with `aria-describedby` while visible; it opens on focus as well as hover.
- The copied confirmation is announced through an `aria-live="polite"` region.
- The dismiss button has `aria-label="Dismiss"` and a 40px hit target.
- Respect `prefers-reduced-motion`: drop the rise, roll and fan-out; keep plain fades.
## Related patterns
- **Prompt-Driven Onboarding** — Use it instead when there is room for the full card with a prompt preview, snippet fallback and first-event status.
- **Setup Checklist** — Use it as well so a dismissed pill's step can live on as a checklist item.
- **Shiny Button** — Use it instead for a different single call to action, and don't show both on one screen.
Do’s and don’ts
Do use it for
- Docs sites, dashboards and quickstarts where many users will set the product up through Claude Code, Cursor, Copilot or similar instead of by hand.
- As a lightweight, always-reachable entry point that sits alongside the regular docs — not instead of them.
- When the setup can be described as one self-contained prompt (install, configure, verify).
Don’t use it for
- Inside a guided onboarding step that needs to wait for a first event — use the Prompt Onboarding card, which shows the prompt and tracks status.
- When the setup needs account-specific secrets the page can't safely put in a prompt on a public page.
- As a general "Ask AI" entry point — this pill only copies a setup prompt; it doesn't open a chat.
Related patterns
Prompt-Driven Onboarding
Use it instead when there is room for the full card with a prompt preview, snippet fallback and first-event status.
Setup Checklist
Use it as well so a dismissed pill's step can live on as a checklist item.
Shiny Button
Use it instead for a different single call to action, and don't show both on one screen.