97 / 122 · Thread
Launcher
The floating entry point, and the panel it opens into.
Installation
npx shadcn@latest add "@assistant-ui/elements-launcher-bubble"
Usage
import { LauncherBubble } from "@/components/elements/launcher-bubble";
<LauncherBubble
open={open}
unread={2}
greeting="Need a hand?"
prompts={prompts}
onToggle={toggle}
/>Props
open*booleanWhether the panel is showing. The trigger icon crossfades either way.
unread*numberBadge count on the closed bubble. Hidden at zero and while open.
greeting*stringOpening line in the panel.
prompts*string[]Ways in, so the first message does not have to be invented.
onToggle() => voidCalled when the bubble is pressed.
onPick(prompt: string) => voidCalled when a starter prompt is chosen.
onStart() => voidCalled from the Start a conversation action, for opening an empty thread.
classNamestringExtra classes merged onto the root.
Source
launcher-bubble.tsx"use client";
import { MessageCircleIcon, XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { field, floating, inkButton, mono } from "./surfaces";
export function LauncherBubble({
open,
unread,
greeting,
prompts,
onToggle,
onPick,
onStart,
className,
}: {
open: boolean;
unread: number;
greeting: string;
prompts: readonly string[];
onToggle?: () => void;
onPick?: (prompt: string) => void;
onStart?: () => void;
className?: string;
}) {
return (
<div
className={cn(
"flex w-full max-w-[19rem] flex-col items-end gap-2.5",
className,
)}
>
{open && (
<div
className={cn(
floating,
"fade-in zoom-in-95 slide-in-from-bottom-2 animate-in flex w-full flex-col gap-3 rounded-[20px] p-4 duration-200",
)}
>
<div className="flex flex-col gap-1">
<span className="text-[13.5px] font-medium">{greeting}</span>
<span className={cn(mono, "text-foreground/30")}>
typically replies in a minute
</span>
</div>
<div className="flex flex-col gap-1.5">
{prompts.map((prompt) => (
<button
key={prompt}
type="button"
onClick={() => onPick?.(prompt)}
className={cn(
field,
"hover:bg-foreground/[0.07] text-foreground/70 rounded-xl px-3 py-2 text-start text-[13px] transition-colors",
)}
>
{prompt}
</button>
))}
</div>
<button
type="button"
onClick={onStart}
className={cn(
inkButton,
"flex h-8 items-center justify-center rounded-full text-xs font-medium",
)}
>
Start a conversation
</button>
</div>
)}
<button
type="button"
aria-expanded={open}
aria-label={open ? "Close the assistant" : "Open the assistant"}
onClick={onToggle}
className={cn(
inkButton,
"relative flex size-12 shrink-0 items-center justify-center rounded-full",
)}
>
<span className="grid">
<MessageCircleIcon
className={cn(
"size-5 transition-[opacity,rotate] duration-200 [grid-area:1/1]",
open && "rotate-90 opacity-0",
)}
/>
<XIcon
className={cn(
"size-5 transition-[opacity,rotate] duration-200 [grid-area:1/1]",
!open && "-rotate-90 opacity-0",
)}
/>
</span>
{unread > 0 && !open && (
<span
className={cn(
mono,
"bg-background text-foreground absolute -top-0.5 -right-0.5 flex size-4 items-center justify-center rounded-full tabular-nums",
)}
>
{unread}
</span>
)}
</button>
</div>
);
}