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.
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 componentsChat 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.jsonSource
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.