AI Message Bubble

Chat bubble for assistant, user, tool, and system messages using the current design system surfaces. Install AI Message Bubble from the VLLNT UI registry with the shadcn CLI.

Report a bug

When to use this in an AI app

Use AI Message Bubble to render each turn in a conversation. Role variants (user, assistant, tool, system) keep an LLM transcript readable and consistent with the rest of your design system.

Browse all AI agent components

Chat bubble for assistant, user, tool, and system messages using the current design system surfaces. Part of the ai family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

Add AI Message Bubble to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/ai-message-bubble.json

Source

import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "../../lib/utils"; import { Avatar, AvatarFallback } from "../avatar/avatar"; import { Badge } from "../badge/badge"; const bubbleVariants = cva( "rounded-2xl border px-4 py-3 shadow-sm transition-colors", { defaultVariants: { messageRole: "assistant", }, variants: { messageRole: { assistant: "border-border bg-card text-card-foreground", system: "border-border/80 bg-muted/60 text-foreground", tool: "border-border bg-muted/40 text-foreground", user: "border-primary/20 bg-primary/10 text-foreground", }, }, }, ); function AIMessageMeta({ author, isUser, status, timestamp, }: { author?: string; isUser: boolean; status?: string; timestamp?: string; }) { return ( <div className={cn( "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", isUser ? "justify-end" : "justify-start", )} > {author ? ( <span className="font-medium text-foreground">{author}</span> ) : null} {timestamp ? <span>{timestamp}</span> : null} {status ? ( <Badge className="rounded-full px-2 py-0 text-[10px]" variant="secondary" > {status} </Badge> ) : null} </div> ); } export type AIMessageBubbleProps = React.ComponentPropsWithoutRef<"div"> & VariantProps<typeof bubbleVariants> & { /** Optional short label describing the speaker. */ author?: string; /** Bubble body content. */ children: React.ReactNode; /** Optional status badge for the message. */ status?: string; /** Optional timestamp or relative time label. */ timestamp?: string; }; const AIMessageBubble = ({ author, children, className, messageRole = "assistant", ref, status, timestamp, ...props }: AIMessageBubbleProps & { ref?: React.Ref<HTMLDivElement> }) => { const resolvedMessageRole = messageRole ?? "assistant"; const isUser = resolvedMessageRole === "user"; const fallbackLabel = (author ?? resolvedMessageRole).charAt(0).toUpperCase(); return ( <div className={cn("flex w-full", isUser ? "justify-end" : "justify-start")} > <div className={cn( "flex w-full max-w-3xl gap-3", isUser ? "flex-row-reverse text-right" : "flex-row", )} > <Avatar className="mt-0.5 size-8 border border-border/70"> <AvatarFallback className="bg-muted text-[11px] font-medium uppercase text-muted-foreground"> {fallbackLabel} </AvatarFallback> </Avatar> <div className={cn( "min-w-0 space-y-2", isUser ? "items-end" : "items-start", )} > <AIMessageMeta author={author} isUser={isUser} status={status} timestamp={timestamp} /> <div className={cn( bubbleVariants({ messageRole: resolvedMessageRole }), className, )} ref={ref} {...props} > <div className="text-sm leading-6 whitespace-pre-wrap"> {children} </div> </div> </div> </div> </div> ); }; AIMessageBubble.displayName = "AIMessageBubble"; export { AIMessageBubble };

Stories

Explore every variant and state in the interactive Storybook:

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Dependencies

  • @vllnt/ui@^0.3.0
  • class-variance-authority